diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..38ea8b5
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,92 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = crlf
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.cs]
+# Indentation
+indent_size = 4
+tab_width = 4
+
+# New line preferences
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Spacing
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+
+# Wrapping
+csharp_preserve_single_line_statements = true
+csharp_preserve_single_line_blocks = true
+
+# Using directives
+csharp_using_directive_placement = outside_namespace:suggestion
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = false
+
+# this. qualification
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+
+# Language keywords vs BCL types
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# var preferences
+csharp_style_var_for_built_in_types = false:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = false:suggestion
+
+# Expression-bodied members
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_properties = true:silent
+
+# Pattern matching
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+
+# Null checking
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
+
+# Code style
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+
+[*.{csproj,props,targets}]
+indent_size = 2
+
+[*.{json,yml,yaml}]
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/src/TailoredApps.Shared.DateTime/DateTimeProvider.cs b/src/TailoredApps.Shared.DateTime/DateTimeProvider.cs
index 3c57b98..9480c66 100644
--- a/src/TailoredApps.Shared.DateTime/DateTimeProvider.cs
+++ b/src/TailoredApps.Shared.DateTime/DateTimeProvider.cs
@@ -1,4 +1,4 @@
-namespace TailoredApps.Shared.DateTime
+namespace TailoredApps.Shared.DateTime
{
///
/// Simple implementation used for
@@ -35,4 +35,4 @@ public class DateTimeProvider : IDateTimeProvider
///
public System.DateTime UtcToday => System.DateTime.UtcNow.Date;
}
-}
\ No newline at end of file
+}
diff --git a/src/TailoredApps.Shared.DateTime/IDateTimeProvider.cs b/src/TailoredApps.Shared.DateTime/IDateTimeProvider.cs
index bde48d5..74a064d 100644
--- a/src/TailoredApps.Shared.DateTime/IDateTimeProvider.cs
+++ b/src/TailoredApps.Shared.DateTime/IDateTimeProvider.cs
@@ -1,4 +1,4 @@
-namespace TailoredApps.Shared.DateTime
+namespace TailoredApps.Shared.DateTime
{
///
/// Simple interface for mocking used for unit testing and configuration in mocks time used in tests.
@@ -30,4 +30,4 @@ public interface IDateTimeProvider
///
System.DateTime UtcToday { get; }
}
-}
\ No newline at end of file
+}
diff --git a/src/TailoredApps.Shared.Email.Models/MailMessage.cs b/src/TailoredApps.Shared.Email.Models/MailMessage.cs
index bd5ad6d..ad5478c 100644
--- a/src/TailoredApps.Shared.Email.Models/MailMessage.cs
+++ b/src/TailoredApps.Shared.Email.Models/MailMessage.cs
@@ -1,33 +1,33 @@
-using System;
-using System.Collections.Generic;
-
-namespace TailoredApps.Shared.Email.Models
-{
- /// Model wiadomości e-mail.
- public class MailMessage
- {
- /// Temat wiadomości.
- public string Topic { get; set; }
-
- /// Nadawca wiadomości.
- public string Sender { get; set; }
-
- /// Odbiorca wiadomości.
- public string Recipent { get; set; }
-
- /// Kopia CC wiadomości.
- public string Copy { get; set; }
-
- /// Treść wiadomości (plain text).
- public string Body { get; set; }
-
- /// Treść wiadomości (HTML).
- public string HtmlBody { get; set; }
-
- /// Załączniki: nazwa pliku → zawartość Base64.
- public Dictionary Attachements { get; set; }
-
- /// Data wysłania wiadomości.
- public DateTimeOffset Date { get; set; }
- }
-}
+using System;
+using System.Collections.Generic;
+
+namespace TailoredApps.Shared.Email.Models
+{
+ /// Represents an e-mail message.
+ public class MailMessage
+ {
+ /// The subject of the message.
+ public string Topic { get; set; }
+
+ /// The sender of the message.
+ public string Sender { get; set; }
+
+ /// The recipient of the message.
+ public string Recipent { get; set; }
+
+ /// The CC (carbon copy) recipient of the message.
+ public string Copy { get; set; }
+
+ /// The plain-text body of the message.
+ public string Body { get; set; }
+
+ /// The HTML body of the message.
+ public string HtmlBody { get; set; }
+
+ /// Attachments as a dictionary mapping file name to Base64-encoded content.
+ public Dictionary Attachements { get; set; }
+
+ /// The date and time the message was sent.
+ public DateTimeOffset Date { get; set; }
+ }
+}
diff --git a/src/TailoredApps.Shared.Email.Office365/AuthenticationConfig.cs b/src/TailoredApps.Shared.Email.Office365/AuthenticationConfig.cs
index d910aa4..7c96c59 100644
--- a/src/TailoredApps.Shared.Email.Office365/AuthenticationConfig.cs
+++ b/src/TailoredApps.Shared.Email.Office365/AuthenticationConfig.cs
@@ -1,8 +1,8 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-using Microsoft.Identity.Web;
using System;
using System.Globalization;
+using Microsoft.Identity.Web;
namespace TailoredApps.Shared.Email.Office365
{
@@ -12,12 +12,13 @@ namespace TailoredApps.Shared.Email.Office365
///
public class AuthenticationConfig
{
+ /// The configuration key used to bind this section from application settings.
public static string ConfigurationKey => "Mail:Providers:Office365";
///
/// instance of Azure AD, for example public Azure or a Sovereign cloud (Azure China, Germany, US government, etc ...)
///
public string Instance { get; set; } = "https://login.microsoftonline.com/{0}";
-
+
///
/// Graph API endpoint, could be public Azure (default) or a Sovereign cloud (US government, etc ...)
///
@@ -35,13 +36,13 @@ public class AuthenticationConfig
/// Guid used by the application to uniquely identify itself to Azure AD
///
public string ClientId { get; set; }
-
+
///
/// MailBox
///
public string MailBox { get; set; }
-
+
///
/// URL of the authority
@@ -74,7 +75,7 @@ public string Authority
///
public CertificateDescription Certificate { get; set; }
-
+
}
diff --git a/src/TailoredApps.Shared.Email.Office365/Office365EmailProvider.cs b/src/TailoredApps.Shared.Email.Office365/Office365EmailProvider.cs
index fabafb3..07fb3c9 100644
--- a/src/TailoredApps.Shared.Email.Office365/Office365EmailProvider.cs
+++ b/src/TailoredApps.Shared.Email.Office365/Office365EmailProvider.cs
@@ -1,4 +1,10 @@
-using MailKit;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Mail;
+using System.Threading.Tasks;
+using MailKit;
using MailKit.Net.Imap;
using MailKit.Search;
using MailKit.Security;
@@ -8,16 +14,14 @@
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using MimeKit;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Net.Mail;
-using System.Threading.Tasks;
using TailoredApps.Shared.Email.Models;
namespace TailoredApps.Shared.Email.Office365
{
+ ///
+ /// Email provider implementation for Office 365 using IMAP with OAuth2 (client-credentials flow).
+ /// Authenticates against Azure AD as a confidential client application.
+ ///
public class Office365EmailProvider : IEmailProvider
{
private readonly IOptions options;
@@ -26,6 +30,11 @@ public class Office365EmailProvider : IEmailProvider
};
private readonly IConfidentialClientApplication confidentialClientApplication;
+ ///
+ /// Initializes a new instance of and builds
+ /// the confidential client application using either a client secret or a certificate.
+ ///
+ /// The Office 365 authentication configuration options.
public Office365EmailProvider(IOptions options)
{
this.options = options;
@@ -85,6 +94,18 @@ private static bool IsAppUsingClientSecret(AuthenticationConfig config)
else
throw new Exception("You must choose between using client secret or certificate. Please update appsettings.json file.");
}
+ ///
+ /// Retrieves e-mail messages from the configured Office 365 mailbox using IMAP with OAuth2.
+ ///
+ ///
+ /// The name of the IMAP sub-folder to search in. Defaults to the inbox when empty.
+ ///
+ /// Optional filter: only messages whose From address contains this value are returned.
+ /// Optional filter: only messages whose To address contains this value are returned.
+ ///
+ /// Optional time-span filter: only messages delivered within the last are returned.
+ ///
+ /// A collection of objects matching the specified criteria.
public async Task> GetMail(string folderName = "", string sender = "", string recipent = "", TimeSpan? fromLast = null)
{
var response = new List();
@@ -163,13 +184,24 @@ private Dictionary GetAttachements(IEnumerable attac
return result;
}
+ /// Sends an e-mail message via Office 365.
+ /// The recipient e-mail address.
+ /// The subject of the message.
+ /// The body content of the message.
+ /// A dictionary of attachment file names mapped to their byte content.
+ /// A string result or identifier for the sent message.
public async Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments)
{
throw new NotImplementedException();
}
}
+ /// Extension methods for registering the Office 365 email provider with the DI container.
public static class Office365EmailProviderExtensions
{
+ ///
+ /// Registers and its configuration options with the service collection.
+ ///
+ /// The to add the services to.
public static void RegisterOffice365Provider(this IServiceCollection services)
{
services.AddOptions();
@@ -178,14 +210,24 @@ public static void RegisterOffice365Provider(this IServiceCollection services)
}
}
+ ///
+ /// Configures options by binding values from the application configuration.
+ ///
public class Office365EmailConfigureOptions : IConfigureOptions
{
private readonly IConfiguration configuration;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The application configuration used to read Office 365 settings.
public Office365EmailConfigureOptions(IConfiguration configuration)
{
this.configuration = configuration;
}
+ /// Populates with values from the application configuration section.
+ /// The instance to configure.
public void Configure(AuthenticationConfig options)
{
var section = configuration.GetSection(AuthenticationConfig.ConfigurationKey).Get();
diff --git a/src/TailoredApps.Shared.Email/EmailServiceToConsolleWritter.cs b/src/TailoredApps.Shared.Email/EmailServiceToConsolleWritter.cs
index 624ce18..05ea21c 100644
--- a/src/TailoredApps.Shared.Email/EmailServiceToConsolleWritter.cs
+++ b/src/TailoredApps.Shared.Email/EmailServiceToConsolleWritter.cs
@@ -1,25 +1,47 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using TailoredApps.Shared.Email.Models;
-
-namespace TailoredApps.Shared.Email
-{
- /// Implementacja wypisująca wiadomości na konsolę (dev/test).
- public class EmailServiceToConsolleWritter : IEmailProvider
- {
- ///
- public async Task> GetMail(string folder = "", string sender = "", string recipent = "", TimeSpan? fromLast = null)
- {
- return new List();
- }
-
- ///
- public async Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments)
- {
- var message = $"recipent: {recipnet}; topic: {topic}; message: {messageBody}";
- Console.WriteLine(message);
- return message;
- }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using TailoredApps.Shared.Email.Models;
+
+namespace TailoredApps.Shared.Email
+{
+ ///
+ /// Implementation of that writes email messages to the console output.
+ /// Intended for development and testing scenarios where actual email delivery is not required.
+ ///
+ public class EmailServiceToConsolleWritter : IEmailProvider
+ {
+ ///
+ /// Returns an empty collection of mail messages.
+ /// This provider does not support retrieving messages and always returns an empty list.
+ ///
+ /// The mailbox folder to retrieve messages from (ignored).
+ /// Filter by sender email address (ignored).
+ /// Filter by recipient email address (ignored).
+ /// Time span to filter messages received within that period (ignored).
+ /// A task that resolves to an empty collection of .
+ public async Task> GetMail(string folder = "", string sender = "", string recipent = "", TimeSpan? fromLast = null)
+ {
+ return new List();
+ }
+
+ ///
+ /// Writes the email details to the standard console output and returns the formatted message string.
+ /// No actual email is sent; this method is intended for local development and debugging.
+ ///
+ /// The recipient email address.
+ /// The subject line of the email.
+ /// The body content of the email.
+ /// A dictionary of attachment file names mapped to their byte content (not used by this provider).
+ ///
+ /// A task that resolves to a formatted string containing the recipient address, topic, and message body
+ /// that was written to the console.
+ ///
+ public async Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments)
+ {
+ var message = $"recipent: {recipnet}; topic: {topic}; message: {messageBody}";
+ Console.WriteLine(message);
+ return message;
+ }
+ }
+}
diff --git a/src/TailoredApps.Shared.Email/IEmailProvider.cs b/src/TailoredApps.Shared.Email/IEmailProvider.cs
index 22b5931..6f8cbea 100644
--- a/src/TailoredApps.Shared.Email/IEmailProvider.cs
+++ b/src/TailoredApps.Shared.Email/IEmailProvider.cs
@@ -1,17 +1,17 @@
-using System;
-using System.Collections.Generic;
-using System.Net.Mail;
-using System.Threading.Tasks;
-
-namespace TailoredApps.Shared.Email
-{
- /// Interfejs dostawcy e-mail — wysyłanie i odbieranie wiadomości.
- public interface IEmailProvider
- {
- /// Wywołanie API.
- Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments);
- /// Wywołanie API.
- Task> GetMail(string folder = "", string sender = "", string recipent = "", TimeSpan? fromLast = null);
-
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Net.Mail;
+using System.Threading.Tasks;
+
+namespace TailoredApps.Shared.Email
+{
+ /// Interfejs dostawcy e-mail — wysyłanie i odbieranie wiadomości.
+ public interface IEmailProvider
+ {
+ /// Wywołanie API.
+ Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments);
+ /// Wywołanie API.
+ Task> GetMail(string folder = "", string sender = "", string recipent = "", TimeSpan? fromLast = null);
+
+ }
+}
diff --git a/src/TailoredApps.Shared.Email/MailMessageBuilder/DefaultMessageBuilder.cs b/src/TailoredApps.Shared.Email/MailMessageBuilder/DefaultMessageBuilder.cs
index bb333b2..b2c949f 100644
--- a/src/TailoredApps.Shared.Email/MailMessageBuilder/DefaultMessageBuilder.cs
+++ b/src/TailoredApps.Shared.Email/MailMessageBuilder/DefaultMessageBuilder.cs
@@ -1,23 +1,43 @@
-using System.Collections.Generic;
-
-namespace TailoredApps.Shared.Email.MailMessageBuilder
-{
- /// Domyślna implementacja — zastępuje tokeny w szablonie.
- public class DefaultMessageBuilder : IMailMessageBuilder
- {
- ///
- public string Build(string templateKey, IDictionary variables, IDictionary templates)
- {
- if (templates.ContainsKey(templateKey))
- {
- var templateTransform = templates[templateKey];
- foreach (var token in variables)
- {
- templateTransform = templateTransform.Replace(token.Key, token.Value);
- }
- return templateTransform;
- }
- throw new KeyNotFoundException("templateKey");
- }
- }
-}
+using System.Collections.Generic;
+
+namespace TailoredApps.Shared.Email.MailMessageBuilder
+{
+ ///
+ /// Default implementation of that builds a message by performing
+ /// simple key-value token replacement within a named template.
+ ///
+ public class DefaultMessageBuilder : IMailMessageBuilder
+ {
+ ///
+ /// Builds an email message body by locating the specified template and replacing each variable
+ /// token with its corresponding value.
+ ///
+ ///
+ /// The key that identifies the template to use within the dictionary.
+ ///
+ ///
+ /// A dictionary whose keys are the token strings to be replaced and whose values are the
+ /// replacement text to substitute into the template.
+ ///
+ ///
+ /// A dictionary mapping template keys to their raw template content strings.
+ ///
+ /// The template content with all variable tokens replaced by their corresponding values.
+ ///
+ /// Thrown when is not found in the dictionary.
+ ///
+ public string Build(string templateKey, IDictionary variables, IDictionary templates)
+ {
+ if (templates.ContainsKey(templateKey))
+ {
+ var templateTransform = templates[templateKey];
+ foreach (var token in variables)
+ {
+ templateTransform = templateTransform.Replace(token.Key, token.Value);
+ }
+ return templateTransform;
+ }
+ throw new KeyNotFoundException("templateKey");
+ }
+ }
+}
diff --git a/src/TailoredApps.Shared.Email/MailMessageBuilder/IMailMessageBuilder.cs b/src/TailoredApps.Shared.Email/MailMessageBuilder/IMailMessageBuilder.cs
index 405ac97..71e7cd8 100644
--- a/src/TailoredApps.Shared.Email/MailMessageBuilder/IMailMessageBuilder.cs
+++ b/src/TailoredApps.Shared.Email/MailMessageBuilder/IMailMessageBuilder.cs
@@ -1,11 +1,11 @@
-using System.Collections.Generic;
-
-namespace TailoredApps.Shared.Email.MailMessageBuilder
-{
- /// Interfejs budowania treści wiadomości e-mail z szablonu.
- public interface IMailMessageBuilder
- {
- /// Buduje treść wiadomości na podstawie klucza szablonu i zmiennych.
- string Build(string templateKey, IDictionary variables, IDictionary templates);
- }
-}
+using System.Collections.Generic;
+
+namespace TailoredApps.Shared.Email.MailMessageBuilder
+{
+ /// Interfejs budowania treści wiadomości e-mail z szablonu.
+ public interface IMailMessageBuilder
+ {
+ /// Buduje treść wiadomości na podstawie klucza szablonu i zmiennych.
+ string Build(string templateKey, IDictionary variables, IDictionary templates);
+ }
+}
diff --git a/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilder.cs b/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilder.cs
index 031fb17..10b17b4 100644
--- a/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilder.cs
+++ b/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilder.cs
@@ -1,53 +1,85 @@
-using Microsoft.Extensions.Options;
-using System.Collections.Generic;
-using System.IO;
-
-namespace TailoredApps.Shared.Email.MailMessageBuilder
-{
- /// Buduje wiadomości e-mail z szablonów z podmianą tokenów.
- public class TokenReplacingMailMessageBuilder : IMailMessageBuilder
- {
- private readonly IOptions options;
-
- /// Inicjalizuje instancję .
- public TokenReplacingMailMessageBuilder(IOptions options)
- {
- this.options = options;
- }
-
- ///
- public string Build(string templateKey, IDictionary variables, IDictionary templates)
- {
- if (templates == null)
- {
- templates = new Dictionary();
- }
-
- if (options != null && options.Value != null && !string.IsNullOrEmpty(options.Value.Location))
- {
- var files = new DirectoryInfo(options.Value.Location).GetFiles($"*.{options.Value.FileExtension}", SearchOption.AllDirectories);
- foreach (var file in files)
- {
- if (!templates.ContainsKey(file.Name))
- {
- var template = file.OpenText().ReadToEnd();
- templates.Add(templateKey, template);
- }
- }
- }
-
- if (templates.ContainsKey(templateKey))
- {
- var template = templates[templateKey];
- foreach (var key in variables.Keys)
- {
- template = template.Replace(@"{{" + key + "}}", variables[key]);
- }
- return template;
- }
-
- throw new KeyNotFoundException("templateKey");
- }
-
- }
-}
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.Extensions.Options;
+
+namespace TailoredApps.Shared.Email.MailMessageBuilder
+{
+ ///
+ /// Implementation of that builds email message bodies
+ /// by loading templates from the file system and replacing {{token}} placeholders
+ /// with the provided variable values.
+ ///
+ public class TokenReplacingMailMessageBuilder : IMailMessageBuilder
+ {
+ private readonly IOptions options;
+
+ ///
+ /// Initializes a new instance of with the specified options.
+ ///
+ ///
+ /// The options that specify the file system location and extension of template files,
+ /// wrapped in an accessor.
+ ///
+ public TokenReplacingMailMessageBuilder(IOptions options)
+ {
+ this.options = options;
+ }
+
+ ///
+ /// Builds an email message body by resolving the named template and replacing all
+ /// {{variableName}} placeholders with their corresponding values.
+ /// If a file-system location is configured in the options, template files are loaded
+ /// from disk and merged into the provided dictionary
+ /// before the lookup is performed.
+ ///
+ ///
+ /// The key that identifies the template to use. When templates are loaded from the file system
+ /// the key must match the file name (including extension).
+ ///
+ ///
+ /// A dictionary whose keys are the token names (without {{}} delimiters) and whose values
+ /// are the replacement strings to substitute into the template.
+ ///
+ ///
+ /// An optional dictionary of pre-loaded templates mapping template keys to their raw content.
+ /// A null value is treated as an empty dictionary.
+ ///
+ /// The resolved template content with all {{token}} placeholders replaced.
+ ///
+ /// Thrown when cannot be found in the resolved templates dictionary.
+ ///
+ public string Build(string templateKey, IDictionary variables, IDictionary templates)
+ {
+ if (templates == null)
+ {
+ templates = new Dictionary();
+ }
+
+ if (options != null && options.Value != null && !string.IsNullOrEmpty(options.Value.Location))
+ {
+ var files = new DirectoryInfo(options.Value.Location).GetFiles($"*.{options.Value.FileExtension}", SearchOption.AllDirectories);
+ foreach (var file in files)
+ {
+ if (!templates.ContainsKey(file.Name))
+ {
+ var template = file.OpenText().ReadToEnd();
+ templates.Add(templateKey, template);
+ }
+ }
+ }
+
+ if (templates.ContainsKey(templateKey))
+ {
+ var template = templates[templateKey];
+ foreach (var key in variables.Keys)
+ {
+ template = template.Replace(@"{{" + key + "}}", variables[key]);
+ }
+ return template;
+ }
+
+ throw new KeyNotFoundException("templateKey");
+ }
+
+ }
+}
diff --git a/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilderOptions.cs b/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilderOptions.cs
index 7d383bd..cf71b23 100644
--- a/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilderOptions.cs
+++ b/src/TailoredApps.Shared.Email/MailMessageBuilder/TokenReplacingMailMessageBuilderOptions.cs
@@ -1,11 +1,21 @@
-namespace TailoredApps.Shared.Email.MailMessageBuilder
-{
- /// Opcje — lokalizacja i rozszerzenie szablonów.
- public class TokenReplacingMailMessageBuilderOptions
- {
- /// Location.
- public string Location { get; set; }
- /// FileExtension.
- public string FileExtension { get; set; }
- }
-}
+namespace TailoredApps.Shared.Email.MailMessageBuilder
+{
+ ///
+ /// Configuration options for .
+ /// Specifies where template files are stored on the file system and which file extension they use.
+ ///
+ public class TokenReplacingMailMessageBuilderOptions
+ {
+ ///
+ /// Gets or sets the absolute or relative path to the directory that contains email template files.
+ /// When this value is set, the builder will load template files from this location at build time.
+ ///
+ public string Location { get; set; }
+
+ ///
+ /// Gets or sets the file extension (without the leading dot) used to filter template files
+ /// within the directory (e.g., "html" or "txt").
+ ///
+ public string FileExtension { get; set; }
+ }
+}
diff --git a/src/TailoredApps.Shared.Email/SmtpEmailProvider.cs b/src/TailoredApps.Shared.Email/SmtpEmailProvider.cs
index 0b1a87e..98ea8aa 100644
--- a/src/TailoredApps.Shared.Email/SmtpEmailProvider.cs
+++ b/src/TailoredApps.Shared.Email/SmtpEmailProvider.cs
@@ -1,119 +1,167 @@
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Net.Mail;
-using System.Threading.Tasks;
-
-namespace TailoredApps.Shared.Email
-{
- /// Implementacja wysyłająca e-maile przez SMTP.
- public class SmtpEmailProvider : IEmailProvider
- {
- private readonly IOptions options;
- /// Inicjalizuje instancję providera.
- public SmtpEmailProvider(IOptions options)
- {
- this.options = options;
- }
-
- ///
- public async Task> GetMail(string folder = "", string sender = "", string recipent = "", TimeSpan? fromLast = null)
- {
- throw new System.NotImplementedException();
- }
-
- ///
- public async Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments)
- {
-
- using (var client = new SmtpClient(options.Value.Host, options.Value.Port))
- {
- client.UseDefaultCredentials = false;
- client.Credentials = new NetworkCredential(options.Value.UserName, options.Value.Password);
- client.EnableSsl = options.Value.EnableSsl;
- client.Port = options.Value.Port;
-
- var mailMessage = new MailMessage
- {
- Sender = new MailAddress(options.Value.From),
- From = new MailAddress(options.Value.From),
- Subject = topic
- };
- if (attachments != null)
- {
- foreach (var attachment in attachments)
- {
- mailMessage.Attachments.Add(new Attachment(new MemoryStream(attachment.Value), attachment.Key));
- }
- }
- if (options.Value.IsProd)
- {
- mailMessage.To.Add(recipnet);
- }
- else
- {
- mailMessage.To.Add(options.Value.CatchAll);
- }
-
- mailMessage.Body = messageBody;
- mailMessage.IsBodyHtml = true;
- mailMessage.BodyEncoding = System.Text.Encoding.UTF8;
- var msgId = $"<{Guid.NewGuid().ToString().Replace(" - ", "")}@{mailMessage.Sender.Host}>";
- mailMessage.Headers.Add(new System.Collections.Specialized.NameValueCollection() { { "Message-ID", msgId } });
- await client.SendMailAsync(mailMessage);
- return msgId;
- }
- }
- }
-
- /// Rozszerzenia DI dla dostawców e-mail SMTP i konsolowego.
- public static class SmtpEmailProviderExtensions
- {
- /// Rejestruje provider i jego zależności w kontenerze DI.
- public static void RegisterSmtpProvider(this IServiceCollection services)
- {
- services.AddOptions();
- services.ConfigureOptions();
- services.AddTransient();
- }
- /// Rejestruje provider i jego zależności w kontenerze DI.
- public static void RegisterConsoleProvider(this IServiceCollection services)
- {
- services.AddOptions();
- services.ConfigureOptions();
- services.AddTransient();
- }
- }
-
-
-
- /// Wczytuje opcje SMTP z konfiguracji aplikacji.
- public class SmtpEmailConfigureOptions : IConfigureOptions
- {
- private readonly IConfiguration configuration;
- /// Inicjalizuje instancję konfiguracji.
- public SmtpEmailConfigureOptions(IConfiguration configuration)
- {
- this.configuration = configuration;
- }
-
- ///
- public void Configure(SmtpEmailServiceOptions options)
- {
- var section = configuration.GetSection(SmtpEmailServiceOptions.ConfigurationKey).Get();
-
- options.Host = section.Host;
- options.Port = section.Port;
- options.Password = section.Password;
- options.EnableSsl = section.EnableSsl;
- options.UserName = section.UserName;
- options.From = section.From;
- options.IsProd = section.IsProd;
- options.CatchAll = section.CatchAll;
- }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Mail;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace TailoredApps.Shared.Email
+{
+ ///
+ /// Implementation of that sends email messages via SMTP.
+ /// Uses for server configuration.
+ ///
+ public class SmtpEmailProvider : IEmailProvider
+ {
+ private readonly IOptions options;
+
+ ///
+ /// Initializes a new instance of with the specified SMTP options.
+ ///
+ /// The SMTP configuration options wrapped in an accessor.
+ public SmtpEmailProvider(IOptions options)
+ {
+ this.options = options;
+ }
+
+ ///
+ /// Retrieves email messages from the mail server. This method is not yet implemented.
+ ///
+ /// The mailbox folder to retrieve messages from.
+ /// Filter by sender email address.
+ /// Filter by recipient email address.
+ /// Time span to filter messages received within that period.
+ /// A task that retrieves a collection of objects.
+ /// Always thrown; this method is not implemented.
+ public async Task> GetMail(string folder = "", string sender = "", string recipent = "", TimeSpan? fromLast = null)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ ///
+ /// Sends an email message via the configured SMTP server.
+ /// In non-production environments the message is redirected to the configured catch-all address.
+ ///
+ /// The intended recipient email address.
+ /// The subject line of the email.
+ /// The HTML body content of the email.
+ ///
+ /// An optional dictionary of attachment file names mapped to their byte content.
+ /// Pass null or an empty dictionary when no attachments are needed.
+ ///
+ ///
+ /// A task that resolves to the RFC 2822 Message-ID header value assigned to the sent message.
+ ///
+ public async Task SendMail(string recipnet, string topic, string messageBody, Dictionary attachments)
+ {
+
+ using (var client = new SmtpClient(options.Value.Host, options.Value.Port))
+ {
+ client.UseDefaultCredentials = false;
+ client.Credentials = new NetworkCredential(options.Value.UserName, options.Value.Password);
+ client.EnableSsl = options.Value.EnableSsl;
+ client.Port = options.Value.Port;
+
+ var mailMessage = new MailMessage
+ {
+ Sender = new MailAddress(options.Value.From),
+ From = new MailAddress(options.Value.From),
+ Subject = topic
+ };
+ if (attachments != null)
+ {
+ foreach (var attachment in attachments)
+ {
+ mailMessage.Attachments.Add(new Attachment(new MemoryStream(attachment.Value), attachment.Key));
+ }
+ }
+ if (options.Value.IsProd)
+ {
+ mailMessage.To.Add(recipnet);
+ }
+ else
+ {
+ mailMessage.To.Add(options.Value.CatchAll);
+ }
+
+ mailMessage.Body = messageBody;
+ mailMessage.IsBodyHtml = true;
+ mailMessage.BodyEncoding = System.Text.Encoding.UTF8;
+ var msgId = $"<{Guid.NewGuid().ToString().Replace(" - ", "")}@{mailMessage.Sender.Host}>";
+ mailMessage.Headers.Add(new System.Collections.Specialized.NameValueCollection() { { "Message-ID", msgId } });
+ await client.SendMailAsync(mailMessage);
+ return msgId;
+ }
+ }
+ }
+
+ ///
+ /// Provides extension methods for registering email provider implementations in the dependency injection container.
+ ///
+ public static class SmtpEmailProviderExtensions
+ {
+ ///
+ /// Registers the and its required dependencies in the DI container.
+ /// Options are loaded from the application configuration using .
+ ///
+ /// The to add the services to.
+ public static void RegisterSmtpProvider(this IServiceCollection services)
+ {
+ services.AddOptions();
+ services.ConfigureOptions();
+ services.AddTransient();
+ }
+
+ ///
+ /// Registers the console provider and its required dependencies in the DI container.
+ /// Options are loaded from the application configuration using .
+ ///
+ /// The to add the services to.
+ public static void RegisterConsoleProvider(this IServiceCollection services)
+ {
+ services.AddOptions();
+ services.ConfigureOptions();
+ services.AddTransient();
+ }
+ }
+
+ ///
+ /// Configures by reading values from the application configuration.
+ /// Implements to integrate with the options infrastructure.
+ ///
+ public class SmtpEmailConfigureOptions : IConfigureOptions
+ {
+ private readonly IConfiguration configuration;
+
+ ///
+ /// Initializes a new instance of with the given application configuration.
+ ///
+ /// The application configuration used to read SMTP settings.
+ public SmtpEmailConfigureOptions(IConfiguration configuration)
+ {
+ this.configuration = configuration;
+ }
+
+ ///
+ /// Populates the provided instance with values
+ /// from the configuration section identified by .
+ ///
+ /// The options instance to configure.
+ public void Configure(SmtpEmailServiceOptions options)
+ {
+ var section = configuration.GetSection(SmtpEmailServiceOptions.ConfigurationKey).Get();
+
+ options.Host = section.Host;
+ options.Port = section.Port;
+ options.Password = section.Password;
+ options.EnableSsl = section.EnableSsl;
+ options.UserName = section.UserName;
+ options.From = section.From;
+ options.IsProd = section.IsProd;
+ options.CatchAll = section.CatchAll;
+ }
+ }
+}
diff --git a/src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs b/src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs
index fcced5b..adbc447 100644
--- a/src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs
+++ b/src/TailoredApps.Shared.Email/SmtpEmailServiceOptions.cs
@@ -1,26 +1,57 @@
-namespace TailoredApps.Shared.Email
-{
-
- /// Opcje konfiguracji dostawcy SMTP.
- public class SmtpEmailServiceOptions
- {
- /// Klucz sekcji konfiguracji.
- public static string ConfigurationKey => "Mail:Providers:Smtp";
- /// Host.
- public string Host { get; set; }
- /// Port.
- public int Port { get; set; }
- /// Password.
- public string Password { get; set; }
- /// EnableSsl.
- public bool EnableSsl { get; set; }
- /// UserName.
- public string UserName { get; set; }
- /// From.
- public string From { get; set; }
- /// IsProd.
- public bool IsProd { get; set; }
- /// CatchAll.
- public string CatchAll { get; set; }
- }
-}
+namespace TailoredApps.Shared.Email
+{
+ ///
+ /// Configuration options for the SMTP email provider.
+ /// Bind this class to the configuration section identified by .
+ ///
+ public class SmtpEmailServiceOptions
+ {
+ ///
+ /// Gets the configuration section key used to bind these options from the application settings.
+ /// The value is "Mail:Providers:Smtp".
+ ///
+ public static string ConfigurationKey => "Mail:Providers:Smtp";
+
+ ///
+ /// Gets or sets the hostname or IP address of the SMTP server.
+ ///
+ public string Host { get; set; }
+
+ ///
+ /// Gets or sets the port number used to connect to the SMTP server.
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// Gets or sets the password used to authenticate with the SMTP server.
+ ///
+ public string Password { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether SSL/TLS encryption is enabled for the SMTP connection.
+ ///
+ public bool EnableSsl { get; set; }
+
+ ///
+ /// Gets or sets the username used to authenticate with the SMTP server.
+ ///
+ public string UserName { get; set; }
+
+ ///
+ /// Gets or sets the email address used as the sender (From/Sender header) of outgoing messages.
+ ///
+ public string From { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the application is running in a production environment.
+ /// When true, emails are sent to the actual recipient; otherwise they are redirected to .
+ ///
+ public bool IsProd { get; set; }
+
+ ///
+ /// Gets or sets the catch-all email address used as the recipient in non-production environments.
+ /// All outgoing messages are redirected to this address when is false.
+ ///
+ public string CatchAll { get; set; }
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore/Attributes/TransactionIsolationLevelAttribute.cs b/src/TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore/Attributes/TransactionIsolationLevelAttribute.cs
index ad54263..f4e7a83 100644
--- a/src/TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore/Attributes/TransactionIsolationLevelAttribute.cs
+++ b/src/TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore/Attributes/TransactionIsolationLevelAttribute.cs
@@ -1,16 +1,25 @@
-using System;
+using System;
using System.Data;
namespace TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore.Attributes
{
+ ///
+ /// Decorates a controller action (or entire controller class) to specify
+ /// the database transaction isolation level that the Unit of Work should apply.
+ ///
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class TransactionIsolationLevelAttribute : Attribute
{
+ /// The isolation level to apply to the database transaction for the decorated action.
public IsolationLevel Level { get; set; }
+ ///
+ /// Initialises the attribute with the specified isolation level.
+ ///
+ /// The to use for the transaction.
public TransactionIsolationLevelAttribute(IsolationLevel level)
{
Level = level;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore/Filters/TransactionFilterAttribute.cs b/src/TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore/Filters/TransactionFilterAttribute.cs
index 82e4d41..8a55848 100644
--- a/src/TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore/Filters/TransactionFilterAttribute.cs
+++ b/src/TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore/Filters/TransactionFilterAttribute.cs
@@ -1,21 +1,36 @@
-using Microsoft.AspNetCore.Mvc.Controllers;
-using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Reflection;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.Filters;
using TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork;
using TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore.Attributes;
namespace TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore.Filters
{
+ ///
+ /// ASP.NET Core action filter that automatically wraps each controller action
+ /// in a Unit of Work transaction.
+ /// Commits on success and rolls back on exception.
+ /// The isolation level can be overridden per-action via .
+ ///
public class TransactionFilterAttribute : ActionFilterAttribute
{
private readonly IUnitOfWork _uow;
+ ///
+ /// Initialises the filter with the Unit of Work instance resolved from the DI container.
+ ///
+ /// The scoped Unit of Work for the current HTTP request.
public TransactionFilterAttribute(IUnitOfWork uow)
{
_uow = uow;
}
+ ///
+ /// Called before the action executes.
+ /// Applies a custom isolation level when the action is decorated with .
+ ///
+ /// The executing action context.
public override void OnActionExecuting(ActionExecutingContext actionContext)
{
//check if the action has explicitly stated which isolation level should be set in unit of work
@@ -31,10 +46,14 @@ public override void OnActionExecuting(ActionExecutingContext actionContext)
_uow.SetIsolationLevel(isolationLevelAttribute.Level);
}
-
base.OnActionExecuting(actionContext);
}
+ ///
+ /// Called after the action executes.
+ /// Commits the transaction on success or rolls it back when an exception occurred.
+ ///
+ /// The executed action context.
public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
{
// We need a container per request, therefore we cannot inject dependencies with StructureMap,
@@ -59,4 +78,4 @@ public override void OnActionExecuted(ActionExecutedContext actionExecutedContex
base.OnActionExecuted(actionExecutedContext);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore/UnitOfWorkConfiguration.cs b/src/TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore/UnitOfWorkConfiguration.cs
index 113a948..7264f88 100644
--- a/src/TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore/UnitOfWorkConfiguration.cs
+++ b/src/TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore/UnitOfWorkConfiguration.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork;
@@ -6,8 +6,19 @@
namespace TailoredApps.Shared.EntityFramework.UnitOfWork.WebApiCore
{
+ ///
+ /// Extension methods for registering the Unit of Work pattern in ASP.NET Core Web API projects.
+ ///
public static class UnitOfWorkConfiguration
{
+ ///
+ /// Registers the Unit of Work and the in the DI container,
+ /// scoped to the current HTTP request.
+ ///
+ /// The interface that must implement.
+ /// The concrete EF Core type.
+ /// The DI service collection to configure.
+ /// An for further configuration (hooks, auditing, etc.).
public static IUnitOfWorkOptionsBuilder AddUnitOfWorkForWebApi(this IServiceCollection services)
where TTargetDbContext : DbContext, TTargetDbContextInterface
where TTargetDbContextInterface : class
@@ -16,9 +27,14 @@ public static IUnitOfWorkOptionsBuilder AddUnitOfWorkForWebApi();
}
+ ///
+ /// Adds as a global MVC filter so that every
+ /// controller action is automatically wrapped in a Unit of Work transaction.
+ ///
+ /// The application's global filter collection.
public static void AddUnitOfWorkTransactionAttribute(this FilterCollection filters)
{
filters.Add();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/AssmeblyInfo.cs b/src/TailoredApps.Shared.EntityFramework/AssmeblyInfo.cs
index 52f412d..733bb5a 100644
--- a/src/TailoredApps.Shared.EntityFramework/AssmeblyInfo.cs
+++ b/src/TailoredApps.Shared.EntityFramework/AssmeblyInfo.cs
@@ -1,4 +1,4 @@
-using System.Runtime.CompilerServices;
+using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("TailoredApps.Shared.EntityFramework.Tests")]
-[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
\ No newline at end of file
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/src/TailoredApps.Shared.EntityFramework/Extensions/EntityTypeBuilderExtension.cs b/src/TailoredApps.Shared.EntityFramework/Extensions/EntityTypeBuilderExtension.cs
index 79ac446..cfd6e93 100644
--- a/src/TailoredApps.Shared.EntityFramework/Extensions/EntityTypeBuilderExtension.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Extensions/EntityTypeBuilderExtension.cs
@@ -1,28 +1,28 @@
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Metadata.Builders;
-using System;
-using TailoredApps.Shared.EntityFramework.Interfaces;
-
-namespace TailoredApps.Shared.EntityFramework.Extensions
-{
- ///
- /// Provides extension methods for to simplify
- /// mapping of shared entity interfaces.
- ///
- public static class EntityTypeBuilderExtension
- {
- ///
- /// Configures the standard activity-tracking columns (CreatedBy, CreatedDateUtc,
- /// ModifiedBy, ModifiedDateUtc) for entities that implement .
- ///
- /// The entity type that implements .
- /// The entity type builder to configure.
- public static void AddIActivity(this EntityTypeBuilder entity) where T : class, IActivity
- {
- entity.Property(t => t.CreatedBy).HasColumnName("CreatedBy").IsRequired().HasMaxLength(321);
- entity.Property(t => t.CreatedDateUtc).HasColumnName("CreatedDateUtc").HasDefaultValue(DateTime.UtcNow).IsRequired();
- entity.Property(t => t.ModifiedBy).HasColumnName("ModifiedBy").HasMaxLength(321);
- entity.Property(t => t.ModifiedDateUtc).HasColumnName("ModifiedDateUtc");
- }
- }
-}
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using TailoredApps.Shared.EntityFramework.Interfaces;
+
+namespace TailoredApps.Shared.EntityFramework.Extensions
+{
+ ///
+ /// Provides extension methods for to simplify
+ /// mapping of shared entity interfaces.
+ ///
+ public static class EntityTypeBuilderExtension
+ {
+ ///
+ /// Configures the standard activity-tracking columns (CreatedBy, CreatedDateUtc,
+ /// ModifiedBy, ModifiedDateUtc) for entities that implement .
+ ///
+ /// The entity type that implements .
+ /// The entity type builder to configure.
+ public static void AddIActivity(this EntityTypeBuilder entity) where T : class, IActivity
+ {
+ entity.Property(t => t.CreatedBy).HasColumnName("CreatedBy").IsRequired().HasMaxLength(321);
+ entity.Property(t => t.CreatedDateUtc).HasColumnName("CreatedDateUtc").HasDefaultValue(DateTime.UtcNow).IsRequired();
+ entity.Property(t => t.ModifiedBy).HasColumnName("ModifiedBy").HasMaxLength(321);
+ entity.Property(t => t.ModifiedDateUtc).HasColumnName("ModifiedDateUtc");
+ }
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/AuditEntityState.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/AuditEntityState.cs
index e794dc1..e2b1840 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/AuditEntityState.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/AuditEntityState.cs
@@ -1,24 +1,24 @@
-namespace TailoredApps.Shared.EntityFramework.Interfaces.Audit
-{
- ///
- /// Represents the audited state of an entity tracked by the Unit of Work audit context.
- /// Mirrors the relevant values of .
- ///
- public enum AuditEntityState
- {
- ///
- /// Microsoft.EntityFrameworkCore.EntityState.Added
- ///
- Added,
-
- ///
- /// Microsoft.EntityFrameworkCore.EntityState.Modified
- ///
- Modified,
-
- ///
- /// Microsoft.EntityFrameworkCore.EntityState.Deleted
- ///
- Deleted,
- }
-}
+namespace TailoredApps.Shared.EntityFramework.Interfaces.Audit
+{
+ ///
+ /// Represents the audited state of an entity tracked by the Unit of Work audit context.
+ /// Mirrors the relevant values of .
+ ///
+ public enum AuditEntityState
+ {
+ ///
+ /// Microsoft.EntityFrameworkCore.EntityState.Added
+ ///
+ Added,
+
+ ///
+ /// Microsoft.EntityFrameworkCore.EntityState.Modified
+ ///
+ Modified,
+
+ ///
+ /// Microsoft.EntityFrameworkCore.EntityState.Deleted
+ ///
+ Deleted,
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/EntityChange.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/EntityChange.cs
index c73d30b..53a31d1 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/EntityChange.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/EntityChange.cs
@@ -1,85 +1,85 @@
-using System;
-using System.Collections.Generic;
-
-namespace TailoredApps.Shared.EntityFramework.Interfaces.Audit
-{
- ///
- /// Abstract base class representing a tracked change to an entity within the audit context.
- /// Captures the entity type, its state, and the primary keys involved in the change.
- ///
- public abstract class EntityChange
- {
- ///
- /// Initializes a new instance of with the specified audit state.
- ///
- /// The of the changed entity.
- protected EntityChange(AuditEntityState state)
- {
- State = state;
- }
-
- ///
- /// Gets the CLR type of the changed entity.
- ///
- public Type EntityType { get; protected set; }
-
- ///
- /// Gets the audit state of the entity (Added, Modified, or Deleted).
- ///
- public AuditEntityState State { get; protected set; }
-
- ///
- /// Gets the original (pre-change) snapshot of the entity as an untyped object.
- ///
- public abstract object Original { get; }
-
- ///
- /// Gets the current (post-change) snapshot of the entity as an untyped object.
- ///
- public abstract object Current { get; }
-
- ///
- /// Gets or sets the dictionary of primary key names and their values for the changed entity.
- ///
- public Dictionary PrimaryKeys { get; set; }
- }
-
- ///
- /// Strongly-typed representation of a tracked entity change.
- ///
- /// The type of the audited entity.
- public class EntityChange : EntityChange
- where TEntity : class
- {
- ///
- /// Initializes a new instance of .
- ///
- /// The current (post-change) state of the entity.
- /// The original (pre-change) state of the entity.
- /// The primary key values of the entity.
- /// The audit state describing the type of change.
- public EntityChange(TEntity currentEntity, TEntity originalEntity, Dictionary keys, AuditEntityState state) : base(state)
- {
- CurrentEntity = currentEntity ?? throw new ArgumentNullException(nameof(currentEntity));
- OriginalEntity = originalEntity ?? throw new ArgumentNullException(nameof(originalEntity));
- EntityType = typeof(TEntity);
- PrimaryKeys = keys;
- }
-
- ///
- /// Gets the original (pre-change) state of the entity.
- ///
- public TEntity OriginalEntity { get; protected set; }
-
- ///
- public override object Original { get => OriginalEntity; }
-
- ///
- /// Gets the current (post-change) state of the entity.
- ///
- public TEntity CurrentEntity { get; protected set; }
-
- ///
- public override object Current { get => CurrentEntity; }
- }
-}
+using System;
+using System.Collections.Generic;
+
+namespace TailoredApps.Shared.EntityFramework.Interfaces.Audit
+{
+ ///
+ /// Abstract base class representing a tracked change to an entity within the audit context.
+ /// Captures the entity type, its state, and the primary keys involved in the change.
+ ///
+ public abstract class EntityChange
+ {
+ ///
+ /// Initializes a new instance of with the specified audit state.
+ ///
+ /// The of the changed entity.
+ protected EntityChange(AuditEntityState state)
+ {
+ State = state;
+ }
+
+ ///
+ /// Gets the CLR type of the changed entity.
+ ///
+ public Type EntityType { get; protected set; }
+
+ ///
+ /// Gets the audit state of the entity (Added, Modified, or Deleted).
+ ///
+ public AuditEntityState State { get; protected set; }
+
+ ///
+ /// Gets the original (pre-change) snapshot of the entity as an untyped object.
+ ///
+ public abstract object Original { get; }
+
+ ///
+ /// Gets the current (post-change) snapshot of the entity as an untyped object.
+ ///
+ public abstract object Current { get; }
+
+ ///
+ /// Gets or sets the dictionary of primary key names and their values for the changed entity.
+ ///
+ public Dictionary PrimaryKeys { get; set; }
+ }
+
+ ///
+ /// Strongly-typed representation of a tracked entity change.
+ ///
+ /// The type of the audited entity.
+ public class EntityChange : EntityChange
+ where TEntity : class
+ {
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The current (post-change) state of the entity.
+ /// The original (pre-change) state of the entity.
+ /// The primary key values of the entity.
+ /// The audit state describing the type of change.
+ public EntityChange(TEntity currentEntity, TEntity originalEntity, Dictionary keys, AuditEntityState state) : base(state)
+ {
+ CurrentEntity = currentEntity ?? throw new ArgumentNullException(nameof(currentEntity));
+ OriginalEntity = originalEntity ?? throw new ArgumentNullException(nameof(originalEntity));
+ EntityType = typeof(TEntity);
+ PrimaryKeys = keys;
+ }
+
+ ///
+ /// Gets the original (pre-change) state of the entity.
+ ///
+ public TEntity OriginalEntity { get; protected set; }
+
+ ///
+ public override object Original { get => OriginalEntity; }
+
+ ///
+ /// Gets the current (post-change) state of the entity.
+ ///
+ public TEntity CurrentEntity { get; protected set; }
+
+ ///
+ public override object Current { get => CurrentEntity; }
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/IAuditSettings.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/IAuditSettings.cs
index 3e4ad6a..8857964 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/IAuditSettings.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/IAuditSettings.cs
@@ -1,25 +1,25 @@
-using System;
-using System.Collections.Generic;
-
-namespace TailoredApps.Shared.EntityFramework.Interfaces.Audit
-{
- ///
- /// Defines the configuration settings that control which entity types and states
- /// are collected by the Unit of Work audit mechanism.
- ///
- public interface IAuditSettings
- {
- ///
- /// Gets or sets the collection of CLR types whose changes should be audited.
- /// Only entities whose type is present in this collection will be tracked.
- ///
- IEnumerable TypesToCollect { get; set; }
-
- ///
- /// Gets or sets the collection of entity states (Added, Modified, Deleted) to include
- /// in the audit. Only state transitions matching an entry in this collection are recorded.
- ///
- IEnumerable EntityStatesToCollect { get; set; }
- }
-
-}
+using System;
+using System.Collections.Generic;
+
+namespace TailoredApps.Shared.EntityFramework.Interfaces.Audit
+{
+ ///
+ /// Defines the configuration settings that control which entity types and states
+ /// are collected by the Unit of Work audit mechanism.
+ ///
+ public interface IAuditSettings
+ {
+ ///
+ /// Gets or sets the collection of CLR types whose changes should be audited.
+ /// Only entities whose type is present in this collection will be tracked.
+ ///
+ IEnumerable TypesToCollect { get; set; }
+
+ ///
+ /// Gets or sets the collection of entity states (Added, Modified, Deleted) to include
+ /// in the audit. Only state transitions matching an entry in this collection are recorded.
+ ///
+ IEnumerable EntityStatesToCollect { get; set; }
+ }
+
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/IEntityChangesAuditor.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/IEntityChangesAuditor.cs
index 31aa010..f6a9c4c 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/IEntityChangesAuditor.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/IEntityChangesAuditor.cs
@@ -1,17 +1,17 @@
-using System.Collections.Generic;
-
-namespace TailoredApps.Shared.EntityFramework.Interfaces.Audit
-{
- ///
- /// Defines the contract for processing and persisting a collection of audited entity changes
- /// after they have been collected by the Unit of Work audit context.
- ///
- public interface IEntityChangesAuditor
- {
- ///
- /// Processes and stores the given entity changes (e.g. writes them to an audit log).
- ///
- /// The collection of entity changes to audit.
- void AuditChanges(IEnumerable entityChanges);
- }
-}
+using System.Collections.Generic;
+
+namespace TailoredApps.Shared.EntityFramework.Interfaces.Audit
+{
+ ///
+ /// Defines the contract for processing and persisting a collection of audited entity changes
+ /// after they have been collected by the Unit of Work audit context.
+ ///
+ public interface IEntityChangesAuditor
+ {
+ ///
+ /// Processes and stores the given entity changes (e.g. writes them to an audit log).
+ ///
+ /// The collection of entity changes to audit.
+ void AuditChanges(IEnumerable entityChanges);
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/IUnitOfWorkAuditContext.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/IUnitOfWorkAuditContext.cs
index 216f1b9..8af66ad 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/IUnitOfWorkAuditContext.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/Audit/IUnitOfWorkAuditContext.cs
@@ -1,32 +1,32 @@
-namespace TailoredApps.Shared.EntityFramework.Interfaces.Audit
-{
- ///
- /// Defines the lifecycle contract for the Unit of Work audit context.
- /// Manages collection, post-processing, discarding, and final auditing of entity changes.
- ///
- public interface IUnitOfWorkAuditContext
- {
- ///
- /// Performs any post-collection processing on the gathered entity changes
- /// (e.g. enriching change records after save).
- ///
- void PostCollectChanges();
-
- ///
- /// Collects the current entity changes from the EF Core change tracker
- /// before the save operation is committed.
- ///
- void CollectChanges();
-
- ///
- /// Discards all previously collected entity changes, typically called on transaction rollback.
- ///
- void DiscardChanges();
-
- ///
- /// Passes the collected entity changes to the registered
- /// for processing and persistence (e.g. after a successful transaction commit).
- ///
- void AuditChanges();
- }
-}
+namespace TailoredApps.Shared.EntityFramework.Interfaces.Audit
+{
+ ///
+ /// Defines the lifecycle contract for the Unit of Work audit context.
+ /// Manages collection, post-processing, discarding, and final auditing of entity changes.
+ ///
+ public interface IUnitOfWorkAuditContext
+ {
+ ///
+ /// Performs any post-collection processing on the gathered entity changes
+ /// (e.g. enriching change records after save).
+ ///
+ void PostCollectChanges();
+
+ ///
+ /// Collects the current entity changes from the EF Core change tracker
+ /// before the save operation is committed.
+ ///
+ void CollectChanges();
+
+ ///
+ /// Discards all previously collected entity changes, typically called on transaction rollback.
+ ///
+ void DiscardChanges();
+
+ ///
+ /// Passes the collected entity changes to the registered
+ /// for processing and persistence (e.g. after a successful transaction commit).
+ ///
+ void AuditChanges();
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/IActivity.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/IActivity.cs
index 17fce9f..c08cf94 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/IActivity.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/IActivity.cs
@@ -1,30 +1,30 @@
-using System;
-
-namespace TailoredApps.Shared.EntityFramework.Interfaces
-{
- ///
- /// Marks an entity as tracking activity metadata: who created or last modified it and when.
- ///
- public interface IActivity
- {
- ///
- /// Gets or sets the UTC date and time when the entity was created.
- ///
- DateTime CreatedDateUtc { get; set; }
-
- ///
- /// Gets or sets the identifier (e.g. username or email) of the user who created the entity.
- ///
- string CreatedBy { get; set; }
-
- ///
- /// Gets or sets the UTC date and time when the entity was last modified, or null if it has never been modified.
- ///
- DateTime? ModifiedDateUtc { get; set; }
-
- ///
- /// Gets or sets the identifier of the user who last modified the entity, or null if it has never been modified.
- ///
- string ModifiedBy { get; set; }
- }
-}
+using System;
+
+namespace TailoredApps.Shared.EntityFramework.Interfaces
+{
+ ///
+ /// Marks an entity as tracking activity metadata: who created or last modified it and when.
+ ///
+ public interface IActivity
+ {
+ ///
+ /// Gets or sets the UTC date and time when the entity was created.
+ ///
+ DateTime CreatedDateUtc { get; set; }
+
+ ///
+ /// Gets or sets the identifier (e.g. username or email) of the user who created the entity.
+ ///
+ string CreatedBy { get; set; }
+
+ ///
+ /// Gets or sets the UTC date and time when the entity was last modified, or null if it has never been modified.
+ ///
+ DateTime? ModifiedDateUtc { get; set; }
+
+ ///
+ /// Gets or sets the identifier of the user who last modified the entity, or null if it has never been modified.
+ ///
+ string ModifiedBy { get; set; }
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/IModelBase.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/IModelBase.cs
index 56d7e21..7f61120 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/IModelBase.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/IModelBase.cs
@@ -1,22 +1,22 @@
-namespace TailoredApps.Shared.EntityFramework.Interfaces
-{
- ///
- /// Strongly-typed base interface for entities that expose an identifier of type .
- ///
- /// The type of the entity's primary key.
- public interface IModelBase : IModelBase
- {
- ///
- /// Gets or sets the primary key of the entity.
- ///
- T Id { get; set; }
- }
-
- ///
- /// Marker interface for all entity model base types in the EntityFramework shared layer.
- /// Used as a constraint for generic query helpers.
- ///
- public interface IModelBase
- {
- }
-}
+namespace TailoredApps.Shared.EntityFramework.Interfaces
+{
+ ///
+ /// Strongly-typed base interface for entities that expose an identifier of type .
+ ///
+ /// The type of the entity's primary key.
+ public interface IModelBase : IModelBase
+ {
+ ///
+ /// Gets or sets the primary key of the entity.
+ ///
+ T Id { get; set; }
+ }
+
+ ///
+ /// Marker interface for all entity model base types in the EntityFramework shared layer.
+ /// Used as a constraint for generic query helpers.
+ ///
+ public interface IModelBase
+ {
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/IModelBuilder.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/IModelBuilder.cs
index f6ce38e..b7e4d10 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/IModelBuilder.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/IModelBuilder.cs
@@ -1,17 +1,17 @@
-using Microsoft.EntityFrameworkCore;
-
-namespace TailoredApps.Shared.EntityFramework.Interfaces
-{
- ///
- /// Defines the contract for a class that contributes EF Core model configuration
- /// to a during the OnModelCreating phase.
- ///
- public interface IModelBuilder
- {
- ///
- /// Applies entity mappings, relationships, and constraints to the provided .
- ///
- /// The EF Core model builder to configure.
- void MapModel(ModelBuilder modelBuilder);
- }
-}
+using Microsoft.EntityFrameworkCore;
+
+namespace TailoredApps.Shared.EntityFramework.Interfaces
+{
+ ///
+ /// Defines the contract for a class that contributes EF Core model configuration
+ /// to a during the OnModelCreating phase.
+ ///
+ public interface IModelBuilder
+ {
+ ///
+ /// Applies entity mappings, relationships, and constraints to the provided .
+ ///
+ /// The EF Core model builder to configure.
+ void MapModel(ModelBuilder modelBuilder);
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IHook.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IHook.cs
index f1afd53..472e439 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IHook.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IHook.cs
@@ -1,36 +1,36 @@
-
-
-namespace TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork
-{
- ///
- /// Base contract for Unit of Work lifecycle hooks.
- /// Implementations are invoked at specific points in the save/transaction lifecycle.
- ///
- public interface IHook
- {
- ///
- /// Executes the hook logic at the appropriate lifecycle point.
- ///
- void Execute();
- }
-
- ///
- /// Hook that is executed immediately after a transaction is successfully committed.
- ///
- public interface ITransactionCommitHook : IHook { }
-
- ///
- /// Hook that is executed immediately after a transaction is rolled back.
- ///
- public interface ITransactionRollbackHook : IHook { }
-
- ///
- /// Hook that is executed immediately after SaveChanges completes successfully.
- ///
- public interface IPostSaveChangesHook : IHook { }
-
- ///
- /// Hook that is executed immediately before SaveChanges is called.
- ///
- public interface IPreSaveChangesHook : IHook { }
-}
+
+
+namespace TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork
+{
+ ///
+ /// Base contract for Unit of Work lifecycle hooks.
+ /// Implementations are invoked at specific points in the save/transaction lifecycle.
+ ///
+ public interface IHook
+ {
+ ///
+ /// Executes the hook logic at the appropriate lifecycle point.
+ ///
+ void Execute();
+ }
+
+ ///
+ /// Hook that is executed immediately after a transaction is successfully committed.
+ ///
+ public interface ITransactionCommitHook : IHook { }
+
+ ///
+ /// Hook that is executed immediately after a transaction is rolled back.
+ ///
+ public interface ITransactionRollbackHook : IHook { }
+
+ ///
+ /// Hook that is executed immediately after SaveChanges completes successfully.
+ ///
+ public interface IPostSaveChangesHook : IHook { }
+
+ ///
+ /// Hook that is executed immediately before SaveChanges is called.
+ ///
+ public interface IPreSaveChangesHook : IHook { }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IHooksManager.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IHooksManager.cs
index a35a22a..6e764be 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IHooksManager.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IHooksManager.cs
@@ -1,33 +1,33 @@
-namespace TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork
-{
- ///
- /// Manages and executes the registered Unit of Work lifecycle hooks
- /// at the appropriate points in the save/transaction cycle.
- ///
- public interface IHooksManager
- {
- ///
- /// Executes all registered implementations
- /// before changes are saved to the database.
- ///
- void ExecutePreSaveChangesHooks();
-
- ///
- /// Executes all registered implementations
- /// after changes have been saved to the database.
- ///
- void ExecutePostSaveChangesHooks();
-
- ///
- /// Executes all registered implementations
- /// after a transaction has been rolled back.
- ///
- void ExecuteTransactionRollbackHooks();
-
- ///
- /// Executes all registered implementations
- /// after a transaction has been committed.
- ///
- void ExecuteTransactionCommitHooks();
- }
-}
+namespace TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork
+{
+ ///
+ /// Manages and executes the registered Unit of Work lifecycle hooks
+ /// at the appropriate points in the save/transaction cycle.
+ ///
+ public interface IHooksManager
+ {
+ ///
+ /// Executes all registered implementations
+ /// before changes are saved to the database.
+ ///
+ void ExecutePreSaveChangesHooks();
+
+ ///
+ /// Executes all registered implementations
+ /// after changes have been saved to the database.
+ ///
+ void ExecutePostSaveChangesHooks();
+
+ ///
+ /// Executes all registered implementations
+ /// after a transaction has been rolled back.
+ ///
+ void ExecuteTransactionRollbackHooks();
+
+ ///
+ /// Executes all registered implementations
+ /// after a transaction has been committed.
+ ///
+ void ExecuteTransactionCommitHooks();
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/ITransaction.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/ITransaction.cs
index 02ade3d..dafb277 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/ITransaction.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/ITransaction.cs
@@ -1,21 +1,21 @@
-using System;
-
-namespace TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork
-{
- ///
- /// Represents an active database transaction managed by the Unit of Work.
- /// Provides commit and rollback operations and must be disposed when no longer needed.
- ///
- public interface ITransaction : IDisposable
- {
- ///
- /// Commits all changes made within this transaction to the database.
- ///
- void Commit();
-
- ///
- /// Rolls back all changes made within this transaction, discarding any pending modifications.
- ///
- void Rollback();
- }
-}
+using System;
+
+namespace TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork
+{
+ ///
+ /// Represents an active database transaction managed by the Unit of Work.
+ /// Provides commit and rollback operations and must be disposed when no longer needed.
+ ///
+ public interface ITransaction : IDisposable
+ {
+ ///
+ /// Commits all changes made within this transaction to the database.
+ ///
+ void Commit();
+
+ ///
+ /// Rolls back all changes made within this transaction, discarding any pending modifications.
+ ///
+ void Rollback();
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IUnitOfWork.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IUnitOfWork.cs
index c2cd1ee..a293ef8 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IUnitOfWork.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IUnitOfWork.cs
@@ -1,80 +1,80 @@
-using System;
-using System.Data;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork
-{
- ///
- /// Defines the core contract for the Unit of Work pattern, providing transaction management
- /// and change persistence over an underlying data provider.
- ///
- public interface IUnitOfWork : IDisposable
- {
- ///
- /// Gets a value indicating whether a database transaction is currently open.
- ///
- bool HasOpenTransaction { get; }
-
- ///
- /// Opens a new transaction if no transaction is currently open.
- /// This method should only be used if you need to open a transaction
- /// in one exact moment. Otherwise, let Unit of Work open it for you
- /// in a convenient time.
- ///
- void BeginTransactionManually();
-
- ///
- /// Commits the current transaction (if one is open)
- ///
- void CommitTransaction();
-
- ///
- /// Commits the current transaction (if one is open) and sets the isolation level for new ones.
- ///
- void CommitTransaction(IsolationLevel isolationLevel);
-
- ///
- /// Rolls back the current transaction (if one is open)
- ///
- void RollbackTransaction();
-
- ///
- /// Rolls back the current transaction (if one is open) and sets the isolation level for new ones.
- ///
- void RollbackTransaction(IsolationLevel isolationLevel);
-
- ///
- /// Saves changes to database. If no transaction has been created yet,
- /// this method will open a new transaction with isolation level set in UoW
- /// before saving changes.
- ///
- int SaveChanges();
-
- ///
- /// Asynchronously saves changes to database. If no transaction has been created yet,
- /// this method will open a new transaction with isolation level set in UoW
- /// before saving changes.
- ///
- Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));
-
- ///
- /// Sets the isolation level for new transactions. This method does not
- /// change the isolation level of the currently open transaction.
- ///
- ///
- void SetIsolationLevel(IsolationLevel isolationLevel);
- }
-
- ///
- /// Extends with access to a typed data provider (e.g. a repository or DbContext facade).
- ///
- /// The type of the data provider exposed by this unit of work.
- public interface IUnitOfWork : IUnitOfWork
- {
- ///
- /// Gets the underlying data provider (e.g. a DbContext interface or repository) for this unit of work.
- ///
- T DataProvider { get; }
- }
-}
+using System;
+using System.Data;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork
+{
+ ///
+ /// Defines the core contract for the Unit of Work pattern, providing transaction management
+ /// and change persistence over an underlying data provider.
+ ///
+ public interface IUnitOfWork : IDisposable
+ {
+ ///
+ /// Gets a value indicating whether a database transaction is currently open.
+ ///
+ bool HasOpenTransaction { get; }
+
+ ///
+ /// Opens a new transaction if no transaction is currently open.
+ /// This method should only be used if you need to open a transaction
+ /// in one exact moment. Otherwise, let Unit of Work open it for you
+ /// in a convenient time.
+ ///
+ void BeginTransactionManually();
+
+ ///
+ /// Commits the current transaction (if one is open)
+ ///
+ void CommitTransaction();
+
+ ///
+ /// Commits the current transaction (if one is open) and sets the isolation level for new ones.
+ ///
+ void CommitTransaction(IsolationLevel isolationLevel);
+
+ ///
+ /// Rolls back the current transaction (if one is open)
+ ///
+ void RollbackTransaction();
+
+ ///
+ /// Rolls back the current transaction (if one is open) and sets the isolation level for new ones.
+ ///
+ void RollbackTransaction(IsolationLevel isolationLevel);
+
+ ///
+ /// Saves changes to database. If no transaction has been created yet,
+ /// this method will open a new transaction with isolation level set in UoW
+ /// before saving changes.
+ ///
+ int SaveChanges();
+
+ ///
+ /// Asynchronously saves changes to database. If no transaction has been created yet,
+ /// this method will open a new transaction with isolation level set in UoW
+ /// before saving changes.
+ ///
+ Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));
+
+ ///
+ /// Sets the isolation level for new transactions. This method does not
+ /// change the isolation level of the currently open transaction.
+ ///
+ ///
+ void SetIsolationLevel(IsolationLevel isolationLevel);
+ }
+
+ ///
+ /// Extends with access to a typed data provider (e.g. a repository or DbContext facade).
+ ///
+ /// The type of the data provider exposed by this unit of work.
+ public interface IUnitOfWork : IUnitOfWork
+ {
+ ///
+ /// Gets the underlying data provider (e.g. a DbContext interface or repository) for this unit of work.
+ ///
+ T DataProvider { get; }
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IUnitOfWorkContext.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IUnitOfWorkContext.cs
index dbe7f05..8676014 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IUnitOfWorkContext.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IUnitOfWorkContext.cs
@@ -1,51 +1,51 @@
-using System.Data;
-using System.Data.Common;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork
-{
- ///
- /// Provides low-level database operations used internally by the Unit of Work implementation:
- /// transaction management, change persistence, change discarding, and raw connection access.
- ///
- public interface IUnitOfWorkContext
- {
- ///
- /// Begins a new database transaction with the default isolation level.
- ///
- /// An representing the open transaction.
- ITransaction BeginTransaction();
-
- ///
- /// Begins a new database transaction with the specified isolation level.
- ///
- /// The isolation level for the transaction.
- /// An representing the open transaction.
- ITransaction BeginTransaction(IsolationLevel isolationLevel);
-
- ///
- /// Saves all pending changes in the current context to the database.
- ///
- /// The number of state entries written to the database.
- int SaveChanges();
-
- ///
- /// Asynchronously saves all pending changes in the current context to the database.
- ///
- /// A token to cancel the operation.
- /// The number of state entries written to the database.
- Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));
-
- ///
- /// Detaches all tracked entities, effectively discarding any unsaved changes.
- ///
- void DiscardChanges();
-
- ///
- /// Returns the underlying used by this context.
- ///
- /// The active database connection.
- DbConnection GetDbConnection();
- }
-}
+using System.Data;
+using System.Data.Common;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork
+{
+ ///
+ /// Provides low-level database operations used internally by the Unit of Work implementation:
+ /// transaction management, change persistence, change discarding, and raw connection access.
+ ///
+ public interface IUnitOfWorkContext
+ {
+ ///
+ /// Begins a new database transaction with the default isolation level.
+ ///
+ /// An representing the open transaction.
+ ITransaction BeginTransaction();
+
+ ///
+ /// Begins a new database transaction with the specified isolation level.
+ ///
+ /// The isolation level for the transaction.
+ /// An representing the open transaction.
+ ITransaction BeginTransaction(IsolationLevel isolationLevel);
+
+ ///
+ /// Saves all pending changes in the current context to the database.
+ ///
+ /// The number of state entries written to the database.
+ int SaveChanges();
+
+ ///
+ /// Asynchronously saves all pending changes in the current context to the database.
+ ///
+ /// A token to cancel the operation.
+ /// The number of state entries written to the database.
+ Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));
+
+ ///
+ /// Detaches all tracked entities, effectively discarding any unsaved changes.
+ ///
+ void DiscardChanges();
+
+ ///
+ /// Returns the underlying used by this context.
+ ///
+ /// The active database connection.
+ DbConnection GetDbConnection();
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IUnitOfWorkOptionsBuilder.cs b/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IUnitOfWorkOptionsBuilder.cs
index 97f54ab..d84fe6c 100644
--- a/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IUnitOfWorkOptionsBuilder.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Interfaces/UnitOfWork/IUnitOfWorkOptionsBuilder.cs
@@ -1,76 +1,76 @@
-using Microsoft.Extensions.DependencyInjection;
-using System;
-
-namespace TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork
-{
- ///
- /// Fluent builder for configuring Unit of Work lifecycle hooks during application startup.
- ///
- public interface IUnitOfWorkOptionsBuilder
- {
- ///
- /// Registers a implementation (resolved via DI).
- ///
- /// The hook implementation type.
- /// The current builder for further chaining.
- IUnitOfWorkOptionsBuilder WithTransactionCommitHook() where THook : class, ITransactionCommitHook;
-
- ///
- /// Registers a implementation using a factory delegate.
- ///
- /// The hook implementation type.
- /// A factory that creates the hook instance.
- /// The current builder for further chaining.
- IUnitOfWorkOptionsBuilder WithTransactionCommitHook(Func implementationFactory) where THook : class, ITransactionCommitHook;
-
- ///
- /// Registers a implementation (resolved via DI).
- ///
- /// The hook implementation type.
- /// The current builder for further chaining.
- IUnitOfWorkOptionsBuilder WithTransactionRollbackHook() where THook : class, ITransactionRollbackHook;
-
- ///
- /// Registers a implementation using a factory delegate.
- ///
- /// The hook implementation type.
- /// A factory that creates the hook instance.
- /// The current builder for further chaining.
- IUnitOfWorkOptionsBuilder WithTransactionRollbackHook(Func implementationFactory) where THook : class, ITransactionRollbackHook;
-
- ///
- /// Registers a implementation (resolved via DI).
- ///
- /// The hook implementation type.
- /// The current builder for further chaining.
- IUnitOfWorkOptionsBuilder WithPreSaveChangesHook() where THook : class, IPreSaveChangesHook;
-
- ///
- /// Registers a implementation using a factory delegate.
- ///
- /// The hook implementation type.
- /// A factory that creates the hook instance.
- /// The current builder for further chaining.
- IUnitOfWorkOptionsBuilder WithPreSaveChangesHook(Func implementationFactory) where THook : class, IPreSaveChangesHook;
-
- ///
- /// Registers a implementation (resolved via DI).
- ///
- /// The hook implementation type.
- /// The current builder for further chaining.
- IUnitOfWorkOptionsBuilder WithPostSaveChangesHook() where THook : class, IPostSaveChangesHook;
-
- ///
- /// Registers a implementation using a factory delegate.
- ///
- /// The hook implementation type.
- /// A factory that creates the hook instance.
- /// The current builder for further chaining.
- IUnitOfWorkOptionsBuilder WithPostSaveChangesHook(Func implementationFactory) where THook : class, IPostSaveChangesHook;
-
- ///
- /// Gets the underlying used for DI registrations.
- ///
- IServiceCollection Services { get; }
- }
-}
+using System;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace TailoredApps.Shared.EntityFramework.Interfaces.UnitOfWork
+{
+ ///
+ /// Fluent builder for configuring Unit of Work lifecycle hooks during application startup.
+ ///
+ public interface IUnitOfWorkOptionsBuilder
+ {
+ ///
+ /// Registers a implementation (resolved via DI).
+ ///
+ /// The hook implementation type.
+ /// The current builder for further chaining.
+ IUnitOfWorkOptionsBuilder WithTransactionCommitHook() where THook : class, ITransactionCommitHook;
+
+ ///
+ /// Registers a implementation using a factory delegate.
+ ///
+ /// The hook implementation type.
+ /// A factory that creates the hook instance.
+ /// The current builder for further chaining.
+ IUnitOfWorkOptionsBuilder WithTransactionCommitHook(Func implementationFactory) where THook : class, ITransactionCommitHook;
+
+ ///
+ /// Registers a implementation (resolved via DI).
+ ///
+ /// The hook implementation type.
+ /// The current builder for further chaining.
+ IUnitOfWorkOptionsBuilder WithTransactionRollbackHook() where THook : class, ITransactionRollbackHook;
+
+ ///
+ /// Registers a implementation using a factory delegate.
+ ///
+ /// The hook implementation type.
+ /// A factory that creates the hook instance.
+ /// The current builder for further chaining.
+ IUnitOfWorkOptionsBuilder WithTransactionRollbackHook(Func implementationFactory) where THook : class, ITransactionRollbackHook;
+
+ ///
+ /// Registers a implementation (resolved via DI).
+ ///
+ /// The hook implementation type.
+ /// The current builder for further chaining.
+ IUnitOfWorkOptionsBuilder WithPreSaveChangesHook() where THook : class, IPreSaveChangesHook;
+
+ ///
+ /// Registers a implementation using a factory delegate.
+ ///
+ /// The hook implementation type.
+ /// A factory that creates the hook instance.
+ /// The current builder for further chaining.
+ IUnitOfWorkOptionsBuilder WithPreSaveChangesHook(Func implementationFactory) where THook : class, IPreSaveChangesHook;
+
+ ///
+ /// Registers a implementation (resolved via DI).
+ ///
+ /// The hook implementation type.
+ /// The current builder for further chaining.
+ IUnitOfWorkOptionsBuilder WithPostSaveChangesHook() where THook : class, IPostSaveChangesHook;
+
+ ///
+ /// Registers a implementation using a factory delegate.
+ ///
+ /// The hook implementation type.
+ /// A factory that creates the hook instance.
+ /// The current builder for further chaining.
+ IUnitOfWorkOptionsBuilder WithPostSaveChangesHook(Func implementationFactory) where THook : class, IPostSaveChangesHook;
+
+ ///
+ /// Gets the underlying used for DI registrations.
+ ///
+ IServiceCollection Services { get; }
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Logging/EFLoggerToConsole.cs b/src/TailoredApps.Shared.EntityFramework/Logging/EFLoggerToConsole.cs
index ddf59dc..248dd05 100644
--- a/src/TailoredApps.Shared.EntityFramework/Logging/EFLoggerToConsole.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Logging/EFLoggerToConsole.cs
@@ -1,76 +1,103 @@
-using Microsoft.EntityFrameworkCore.Storage;
-using Microsoft.Extensions.Logging;
-using System;
-
-namespace TailoredApps.Shared.EntityFramework.Logging
-{
- ///
- /// An that routes Entity Framework Core relational command
- /// log entries to the console. All other categories are silenced via a no-op logger.
- ///
- public class EFLoggerToConsole : ILoggerProvider
- {
- ///
- /// Creates an for the given category.
- /// Returns a console logger for EF Core relational commands and a no-op logger for everything else.
- ///
- /// The logger category name.
- /// An instance appropriate for the category.
- public ILogger CreateLogger(string categoryName)
- {
- // NOTE: This sample uses EF Core 1.1. If using EF Core 1.0, then use
- // Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommandBuilderFactory
- // rather than IRelationalCommandBuilderFactory
-
- if (categoryName == typeof(RelationalCommandBuilderFactory).FullName)
- {
- return new EFConsoleLogger();
- }
-
- return new NullLogger();
- }
-
- ///
- /// Releases all resources used by this provider.
- ///
- public void Dispose()
- { }
-
- private class EFConsoleLogger : ILogger
- {
- public bool IsEnabled(LogLevel logLevel)
- {
- return true;
- }
-
- public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
- {
- Console.WriteLine(formatter(state, exception));
- }
-
- public IDisposable BeginScope(TState state)
- {
- return null;
- }
- }
-
- private class NullLogger : ILogger
- {
- public bool IsEnabled(LogLevel logLevel)
- {
- return false;
- }
-
- public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
- {
- Console.WriteLine(formatter(state, exception));
-
- }
-
- public IDisposable BeginScope(TState state)
- {
- return null;
- }
- }
- }
-}
+using System;
+using Microsoft.EntityFrameworkCore.Storage;
+using Microsoft.Extensions.Logging;
+
+namespace TailoredApps.Shared.EntityFramework.Logging
+{
+ ///
+ /// An that routes Entity Framework Core relational command
+ /// log entries to the console. All other categories are silenced via a no-op logger.
+ ///
+ public class EFLoggerToConsole : ILoggerProvider
+ {
+ ///
+ /// Creates an for the given category.
+ /// Returns a console logger for EF Core relational commands and a no-op logger for everything else.
+ ///
+ /// The logger category name.
+ /// An instance appropriate for the category.
+ public ILogger CreateLogger(string categoryName)
+ {
+ // NOTE: This sample uses EF Core 1.1. If using EF Core 1.0, then use
+ // Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommandBuilderFactory
+ // rather than IRelationalCommandBuilderFactory
+
+ if (categoryName == typeof(RelationalCommandBuilderFactory).FullName)
+ {
+ return new EFConsoleLogger();
+ }
+
+ return new NullLogger();
+ }
+
+ ///
+ /// Releases all resources used by this provider.
+ ///
+ public void Dispose()
+ { }
+
+ ///
+ /// An implementation that writes all log entries to the console.
+ /// Used for EF Core relational command logging.
+ ///
+ private class EFConsoleLogger : ILogger
+ {
+ ///
+ /// Always returns true; all log levels are enabled.
+ ///
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return true;
+ }
+
+ ///
+ /// Formats the log entry using and writes it to the console.
+ ///
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ Console.WriteLine(formatter(state, exception));
+ }
+
+ ///
+ /// Begins a logical operation scope. Returns null (no-op).
+ ///
+ public IDisposable BeginScope(TState state)
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// A no-op implementation that discards all log entries.
+ /// Used for all categories other than EF Core relational commands.
+ ///
+ private class NullLogger : ILogger
+ {
+ ///
+ /// Always returns false; all log levels are disabled.
+ ///
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return false;
+ }
+
+ ///
+ /// No-op log method. Writes the formatted message to the console even though
+ /// returns false — kept for legacy compatibility.
+ ///
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ Console.WriteLine(formatter(state, exception));
+
+ }
+
+ ///
+ /// Begins a logical operation scope. Returns null (no-op).
+ ///
+ public IDisposable BeginScope(TState state)
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Querying/PagedResult.cs b/src/TailoredApps.Shared.EntityFramework/Querying/PagedResult.cs
index 7d529f2..5f1ea44 100644
--- a/src/TailoredApps.Shared.EntityFramework/Querying/PagedResult.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Querying/PagedResult.cs
@@ -1,93 +1,93 @@
-using Microsoft.EntityFrameworkCore;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using TailoredApps.Shared.Querying;
-
-namespace TailoredApps.Shared.EntityFramework.Querying
-{
- ///
- /// Represents a single page of query results along with the total record count.
- /// Can be constructed from a for deferred execution
- /// or directly from an in-memory list.
- ///
- /// The type of items in the paged result.
- public class PagedResult : IPagedResult
- {
- private static readonly IEnumerable EmptyList = Enumerable.Empty();
- private readonly PagingQuery pagingQuery;
-
- ///
- /// Initializes a new instance of backed by a .
- /// Call or to execute the query.
- ///
- /// The paging query that will provide the results.
- public PagedResult(PagingQuery pagingQuery)
- {
- if (pagingQuery == null) throw new ArgumentNullException(nameof(pagingQuery));
- this.pagingQuery = pagingQuery;
- }
-
- ///
- /// Initializes a new instance of from an already-materialized list.
- ///
- /// The list of items for this page.
- ///
- /// The total number of records across all pages. Defaults to the size of
- /// when not provided.
- ///
- public PagedResult(List results, int? count = null)
- {
- Results = results ?? throw new ArgumentNullException(nameof(results));
- Count = count ?? results.Count;
- }
-
- ///
- /// Asynchronously executes the underlying and populates
- /// and .
- ///
- /// This instance with and populated.
- public async Task> GetPagedResultAsync()
- {
- Results = pagingQuery.IsMoreDataToFetch ? await pagingQuery.ToListAsync() : EmptyList.ToList();
- Count = pagingQuery.TotalCount > 0 ? pagingQuery.TotalCount : Results.Count;
- return this;
- }
-
- ///
- /// Synchronously executes the underlying and populates
- /// and .
- ///
- /// This instance with and populated.
- public PagedResult GetPagedResult()
- {
- if (pagingQuery == null) throw new ArgumentNullException(nameof(pagingQuery));
-
-
- Results = pagingQuery.IsMoreDataToFetch ? pagingQuery.ToList() : EmptyList.ToList();
- Count = pagingQuery.TotalCount > 0 ? pagingQuery.TotalCount : Results.Count;
- return this;
- }
-
- private PagedResult()
- {
- Results = EmptyList.ToList();
- }
-
- ///
- /// Gets or sets the collection of items for the current page.
- ///
- public ICollection Results { get; set; }
-
- ///
- /// Gets or sets the total number of records across all pages.
- ///
- public int Count { get; set; }
-
- ///
- /// Gets an empty with no results and a count of zero.
- ///
- public static PagedResult Empty => new PagedResult();
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+using TailoredApps.Shared.Querying;
+
+namespace TailoredApps.Shared.EntityFramework.Querying
+{
+ ///
+ /// Represents a single page of query results along with the total record count.
+ /// Can be constructed from a for deferred execution
+ /// or directly from an in-memory list.
+ ///
+ /// The type of items in the paged result.
+ public class PagedResult : IPagedResult
+ {
+ private static readonly IEnumerable EmptyList = Enumerable.Empty();
+ private readonly PagingQuery pagingQuery;
+
+ ///
+ /// Initializes a new instance of backed by a .
+ /// Call or to execute the query.
+ ///
+ /// The paging query that will provide the results.
+ public PagedResult(PagingQuery pagingQuery)
+ {
+ if (pagingQuery == null) throw new ArgumentNullException(nameof(pagingQuery));
+ this.pagingQuery = pagingQuery;
+ }
+
+ ///
+ /// Initializes a new instance of from an already-materialized list.
+ ///
+ /// The list of items for this page.
+ ///
+ /// The total number of records across all pages. Defaults to the size of
+ /// when not provided.
+ ///
+ public PagedResult(List results, int? count = null)
+ {
+ Results = results ?? throw new ArgumentNullException(nameof(results));
+ Count = count ?? results.Count;
+ }
+
+ ///
+ /// Asynchronously executes the underlying and populates
+ /// and .
+ ///
+ /// This instance with and populated.
+ public async Task> GetPagedResultAsync()
+ {
+ Results = pagingQuery.IsMoreDataToFetch ? await pagingQuery.ToListAsync() : EmptyList.ToList();
+ Count = pagingQuery.TotalCount > 0 ? pagingQuery.TotalCount : Results.Count;
+ return this;
+ }
+
+ ///
+ /// Synchronously executes the underlying and populates
+ /// and .
+ ///
+ /// This instance with and populated.
+ public PagedResult GetPagedResult()
+ {
+ if (pagingQuery == null) throw new ArgumentNullException(nameof(pagingQuery));
+
+
+ Results = pagingQuery.IsMoreDataToFetch ? pagingQuery.ToList() : EmptyList.ToList();
+ Count = pagingQuery.TotalCount > 0 ? pagingQuery.TotalCount : Results.Count;
+ return this;
+ }
+
+ private PagedResult()
+ {
+ Results = EmptyList.ToList();
+ }
+
+ ///
+ /// Gets or sets the collection of items for the current page.
+ ///
+ public ICollection Results { get; set; }
+
+ ///
+ /// Gets or sets the total number of records across all pages.
+ ///
+ public int Count { get; set; }
+
+ ///
+ /// Gets an empty with no results and a count of zero.
+ ///
+ public static PagedResult Empty => new PagedResult();
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Querying/PagingQuery.cs b/src/TailoredApps.Shared.EntityFramework/Querying/PagingQuery.cs
index 21be5c5..713e04a 100644
--- a/src/TailoredApps.Shared.EntityFramework/Querying/PagingQuery.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Querying/PagingQuery.cs
@@ -1,122 +1,122 @@
-using Microsoft.EntityFrameworkCore;
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-using System.Linq.Expressions;
-using System.Threading.Tasks;
-using TailoredApps.Shared.Querying;
-
-namespace TailoredApps.Shared.EntityFramework.Querying
-{
- ///
- /// Wraps an with paging information, applying skip/take logic
- /// based on the provided . Implements
- /// so it can be consumed directly by EF Core materialization methods.
- ///
- /// The type of elements in the query.
- public class PagingQuery : IQueryable
- {
- private readonly IPagingParameters pagingParameters;
-
- ///
- /// Initializes a new instance of .
- ///
- /// The source queryable to page.
- /// The paging parameters (page number and page size).
- public PagingQuery(IQueryable query, IPagingParameters pagingParameters)
- {
- if (pagingParameters == null)
- throw new ArgumentNullException(nameof(pagingParameters));
-
- if (query == null)
- throw new ArgumentNullException(nameof(query));
-
- Query = query;
- this.pagingParameters = pagingParameters;
-
- }
-
- ///
- /// Asynchronously counts total records and applies skip/take to
- /// when paging parameters are specified.
- ///
- /// This instance with paging applied.
- public async Task> GetPagingQueryAsync()
- {
-
- TotalCount = await Query.CountAsync();
- if (pagingParameters.IsPagingSpecified)
- {
- PageCount = pagingParameters.Count.Value;
- PageNumber = pagingParameters.Page.Value;
- Query = Query.Skip(InternalPageNumber * PageCount).Take(PageCount);
- }
- return this;
- }
-
- ///
- /// Synchronously counts total records and applies skip/take to
- /// when paging parameters are specified.
- ///
- /// This instance with paging applied.
- public PagingQuery GetPagingQuery()
- {
-
- TotalCount = Query.Count();
- if (pagingParameters.IsPagingSpecified)
- {
- PageCount = pagingParameters.Count.Value;
- PageNumber = pagingParameters.Page.Value;
- Query = Query.Skip(InternalPageNumber * PageCount).Take(PageCount);
- }
- return this;
- }
-
- ///
- /// Gets the underlying queryable with optional skip/take applied.
- ///
- public IQueryable Query { get; private set; }
-
- ///
- /// Gets the 1-based page number requested.
- ///
- public int PageNumber { get; private set; }
-
- ///
- /// Gets the number of items per page.
- ///
- public int PageCount { get; private set; }
-
- ///
- /// Gets the total number of records in the unpaged query.
- ///
- public int TotalCount { get; private set; }
-
- ///
- /// Gets a value indicating whether there are more records to fetch for the requested page.
- ///
-#if DEBUG
- public bool IsMoreDataToFetch => TotalCount < PageCount || InternalPageNumber * PageCount <= TotalCount;
-#else
- public bool IsMoreDataToFetch => TotalCount > 0 && (TotalCount < PageCount || InternalPageNumber * PageCount <= TotalCount);
-#endif
-
- ///
- public IEnumerator GetEnumerator() => Query.GetEnumerator();
-
- ///
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
-
- ///
- public Expression Expression => Query.Expression;
-
- ///
- public Type ElementType => Query.ElementType;
-
- ///
- public IQueryProvider Provider => Query.Provider;
-
- private int InternalPageNumber => PageNumber - 1;
- }
-}
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+using TailoredApps.Shared.Querying;
+
+namespace TailoredApps.Shared.EntityFramework.Querying
+{
+ ///
+ /// Wraps an with paging information, applying skip/take logic
+ /// based on the provided . Implements
+ /// so it can be consumed directly by EF Core materialization methods.
+ ///
+ /// The type of elements in the query.
+ public class PagingQuery : IQueryable
+ {
+ private readonly IPagingParameters pagingParameters;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The source queryable to page.
+ /// The paging parameters (page number and page size).
+ public PagingQuery(IQueryable query, IPagingParameters pagingParameters)
+ {
+ if (pagingParameters == null)
+ throw new ArgumentNullException(nameof(pagingParameters));
+
+ if (query == null)
+ throw new ArgumentNullException(nameof(query));
+
+ Query = query;
+ this.pagingParameters = pagingParameters;
+
+ }
+
+ ///
+ /// Asynchronously counts total records and applies skip/take to
+ /// when paging parameters are specified.
+ ///
+ /// This instance with paging applied.
+ public async Task> GetPagingQueryAsync()
+ {
+
+ TotalCount = await Query.CountAsync();
+ if (pagingParameters.IsPagingSpecified)
+ {
+ PageCount = pagingParameters.Count.Value;
+ PageNumber = pagingParameters.Page.Value;
+ Query = Query.Skip(InternalPageNumber * PageCount).Take(PageCount);
+ }
+ return this;
+ }
+
+ ///
+ /// Synchronously counts total records and applies skip/take to
+ /// when paging parameters are specified.
+ ///
+ /// This instance with paging applied.
+ public PagingQuery GetPagingQuery()
+ {
+
+ TotalCount = Query.Count();
+ if (pagingParameters.IsPagingSpecified)
+ {
+ PageCount = pagingParameters.Count.Value;
+ PageNumber = pagingParameters.Page.Value;
+ Query = Query.Skip(InternalPageNumber * PageCount).Take(PageCount);
+ }
+ return this;
+ }
+
+ ///
+ /// Gets the underlying queryable with optional skip/take applied.
+ ///
+ public IQueryable Query { get; private set; }
+
+ ///
+ /// Gets the 1-based page number requested.
+ ///
+ public int PageNumber { get; private set; }
+
+ ///
+ /// Gets the number of items per page.
+ ///
+ public int PageCount { get; private set; }
+
+ ///
+ /// Gets the total number of records in the unpaged query.
+ ///
+ public int TotalCount { get; private set; }
+
+ ///
+ /// Gets a value indicating whether there are more records to fetch for the requested page.
+ ///
+#if DEBUG
+ public bool IsMoreDataToFetch => TotalCount < PageCount || InternalPageNumber * PageCount <= TotalCount;
+#else
+ public bool IsMoreDataToFetch => TotalCount > 0 && (TotalCount < PageCount || InternalPageNumber * PageCount <= TotalCount);
+#endif
+
+ ///
+ public IEnumerator GetEnumerator() => Query.GetEnumerator();
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ ///
+ public Expression Expression => Query.Expression;
+
+ ///
+ public Type ElementType => Query.ElementType;
+
+ ///
+ public IQueryProvider Provider => Query.Provider;
+
+ private int InternalPageNumber => PageNumber - 1;
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Querying/QueryFilterExtensions.cs b/src/TailoredApps.Shared.EntityFramework/Querying/QueryFilterExtensions.cs
index f715091..6f9427d 100644
--- a/src/TailoredApps.Shared.EntityFramework/Querying/QueryFilterExtensions.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Querying/QueryFilterExtensions.cs
@@ -1,36 +1,36 @@
-using System;
-using System.Linq;
-using System.Linq.Expressions;
-using TailoredApps.Shared.EntityFramework.Interfaces;
-
-namespace TailoredApps.Shared.EntityFramework.Querying
-{
- ///
- /// Provides extension methods for filtering sequences of
- /// entities.
- ///
- public static class QueryFilterExtensions
- {
- ///
- /// Applies an optional filter expression to the query.
- /// If is null, the original query is returned unchanged.
- ///
- /// The entity type, constrained to .
- /// The source queryable to filter.
- ///
- /// The predicate expression to apply, or null to skip filtering.
- ///
- /// The filtered (or original) queryable.
- public static IQueryable Filter(
- this IQueryable query,
- Expression> filter)
- where T : IModelBase
- {
- if (filter == null)
- return query;
-
- return query.Where(filter);
- }
- }
-
-}
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using TailoredApps.Shared.EntityFramework.Interfaces;
+
+namespace TailoredApps.Shared.EntityFramework.Querying
+{
+ ///
+ /// Provides extension methods for filtering sequences of
+ /// entities.
+ ///
+ public static class QueryFilterExtensions
+ {
+ ///
+ /// Applies an optional filter expression to the query.
+ /// If is null, the original query is returned unchanged.
+ ///
+ /// The entity type, constrained to .
+ /// The source queryable to filter.
+ ///
+ /// The predicate expression to apply, or null to skip filtering.
+ ///
+ /// The filtered (or original) queryable.
+ public static IQueryable Filter(
+ this IQueryable query,
+ Expression> filter)
+ where T : IModelBase
+ {
+ if (filter == null)
+ return query;
+
+ return query.Where(filter);
+ }
+ }
+
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Querying/QueryPagingExtension.cs b/src/TailoredApps.Shared.EntityFramework/Querying/QueryPagingExtension.cs
index df84ad4..0895d46 100644
--- a/src/TailoredApps.Shared.EntityFramework/Querying/QueryPagingExtension.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Querying/QueryPagingExtension.cs
@@ -1,63 +1,63 @@
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using TailoredApps.Shared.Querying;
-
-namespace TailoredApps.Shared.EntityFramework.Querying
-{
- ///
- /// Provides extension methods for applying paging to sequences
- /// and projecting instances to a different type.
- ///
- public static class QueryPagingExtension
- {
- ///
- /// Synchronously applies paging to the query and returns a .
- ///
- /// The element type of the query.
- /// The source queryable.
- /// The paging parameters specifying page number and page size.
- /// A containing the requested page and total count.
- public static PagedResult Paging(this IQueryable query, IPagingParameters paging)
- {
- var pagingQuery = new PagingQuery(query, paging).GetPagingQuery();
-
- return new PagedResult(pagingQuery).GetPagedResult();
- }
-
- ///
- /// Asynchronously applies paging to the query and returns a .
- ///
- /// The element type of the query.
- /// The source queryable.
- /// The paging parameters specifying page number and page size.
- ///
- /// A task that resolves to a containing the requested page and total count.
- ///
- public static async Task> PagingAsync(this IQueryable query, IPagingParameters paging)
- {
- var pagingQuery = await new PagingQuery(query, paging).GetPagingQueryAsync();
- var result = await new PagedResult(pagingQuery).GetPagedResultAsync();
- return result;
- }
-
- ///
- /// Projects the items of a to a new type
- /// using the provided projector function, preserving the total count.
- ///
- /// The source item type.
- /// The destination item type.
- /// The source paged result to project.
- /// A function that maps each source item to the destination type.
- /// A new with projected items and the original count.
- public static PagedResult Project(this PagedResult pagedResult, Func projector)
- {
- if (pagedResult == null) throw new ArgumentNullException(nameof(pagedResult));
- if (projector == null) throw new ArgumentNullException(nameof(projector));
-
- var destinationModels = pagedResult.Results.Select(projector).ToList();
-
- return new PagedResult(destinationModels, pagedResult.Count);
- }
- }
-}
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using TailoredApps.Shared.Querying;
+
+namespace TailoredApps.Shared.EntityFramework.Querying
+{
+ ///
+ /// Provides extension methods for applying paging to sequences
+ /// and projecting instances to a different type.
+ ///
+ public static class QueryPagingExtension
+ {
+ ///
+ /// Synchronously applies paging to the query and returns a .
+ ///
+ /// The element type of the query.
+ /// The source queryable.
+ /// The paging parameters specifying page number and page size.
+ /// A containing the requested page and total count.
+ public static PagedResult Paging(this IQueryable query, IPagingParameters paging)
+ {
+ var pagingQuery = new PagingQuery(query, paging).GetPagingQuery();
+
+ return new PagedResult(pagingQuery).GetPagedResult();
+ }
+
+ ///
+ /// Asynchronously applies paging to the query and returns a .
+ ///
+ /// The element type of the query.
+ /// The source queryable.
+ /// The paging parameters specifying page number and page size.
+ ///
+ /// A task that resolves to a containing the requested page and total count.
+ ///
+ public static async Task> PagingAsync(this IQueryable query, IPagingParameters paging)
+ {
+ var pagingQuery = await new PagingQuery(query, paging).GetPagingQueryAsync();
+ var result = await new PagedResult(pagingQuery).GetPagedResultAsync();
+ return result;
+ }
+
+ ///
+ /// Projects the items of a to a new type
+ /// using the provided projector function, preserving the total count.
+ ///
+ /// The source item type.
+ /// The destination item type.
+ /// The source paged result to project.
+ /// A function that maps each source item to the destination type.
+ /// A new with projected items and the original count.
+ public static PagedResult Project(this PagedResult pagedResult, Func projector)
+ {
+ if (pagedResult == null) throw new ArgumentNullException(nameof(pagedResult));
+ if (projector == null) throw new ArgumentNullException(nameof(projector));
+
+ var destinationModels = pagedResult.Results.Select(projector).ToList();
+
+ return new PagedResult(destinationModels, pagedResult.Count);
+ }
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/Querying/QuerySortingExtensions.cs b/src/TailoredApps.Shared.EntityFramework/Querying/QuerySortingExtensions.cs
index ac81402..f6f8f55 100644
--- a/src/TailoredApps.Shared.EntityFramework/Querying/QuerySortingExtensions.cs
+++ b/src/TailoredApps.Shared.EntityFramework/Querying/QuerySortingExtensions.cs
@@ -1,78 +1,78 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Linq.Dynamic.Core;
-using TailoredApps.Shared.EntityFramework.Interfaces;
-using TailoredApps.Shared.Querying;
-
-namespace TailoredApps.Shared.EntityFramework.Querying
-{
- ///
- /// Provides extension methods for applying dynamic sorting to sequences.
- ///
- public static class QuerySortingExtensions
- {
- ///
- /// Applies a single set of sorting parameters to the query.
- /// Returns the original query unchanged if is null
- /// or has no sorting specified.
- ///
- /// The element type of the query.
- /// The source queryable.
- /// The sorting parameters to apply.
- /// The sorted (or original) queryable.
- public static IQueryable ApplySorting(
- this IQueryable query,
- ISortingParameters sortingParameters)
- {
- return sortingParameters?.IsSortingSpecified == true
- ? query.OrderBy(GenerateSortQuery(sortingParameters))
- : query;
- }
-
- ///
- /// Applies multiple sets of sorting parameters to the query.
- /// Returns the original query unchanged if no valid sorting parameters are provided.
- ///
- /// The element type of the query, constrained to .
- /// The source queryable.
- /// The collection of sorting parameters to apply in order.
- /// The sorted (or original) queryable.
- public static IQueryable ApplySorting(
- this IQueryable query,
- IEnumerable sortingParameters)
- where T : IModelBase
- {
- var parametersSnapshot = sortingParameters?.Where(x => x.IsSortingSpecified)
- .ToList() ?? Enumerable.Empty().ToList();
-
- return parametersSnapshot.Count > 0
- ? query.OrderBy(GenerateSortQuery(parametersSnapshot))
- : query;
- }
-
- ///
- /// Applies an optional decorator function to the query (e.g. for custom Include or Where clauses).
- /// Returns the original query unchanged if is null.
- ///
- /// The element type of the query.
- /// The source queryable.
- /// An optional function that transforms the query.
- /// The decorated (or original) queryable.
- public static IQueryable AdditionOperation(this IQueryable query,
- Func, IQueryable> decorator)
- => decorator?.Invoke(query) ?? query;
-
- private static string GenerateSortQuery(IEnumerable parameters)
- {
- return string.Join(",", parameters.Where(x => x.IsSortingSpecified)
- .Select(GenerateSortQuery));
- }
-
- private static string GenerateSortQuery(ISortingParameters sortingParameter)
- => sortingParameter.SortDir == SortDirection.Desc
- ? $"{sortingParameter.SortField} {SortDirection.Desc}"
- : sortingParameter.SortField;
-
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Dynamic.Core;
+using TailoredApps.Shared.EntityFramework.Interfaces;
+using TailoredApps.Shared.Querying;
+
+namespace TailoredApps.Shared.EntityFramework.Querying
+{
+ ///
+ /// Provides extension methods for applying dynamic sorting to sequences.
+ ///
+ public static class QuerySortingExtensions
+ {
+ ///
+ /// Applies a single set of sorting parameters to the query.
+ /// Returns the original query unchanged if is null
+ /// or has no sorting specified.
+ ///
+ /// The element type of the query.
+ /// The source queryable.
+ /// The sorting parameters to apply.
+ /// The sorted (or original) queryable.
+ public static IQueryable ApplySorting(
+ this IQueryable query,
+ ISortingParameters sortingParameters)
+ {
+ return sortingParameters?.IsSortingSpecified == true
+ ? query.OrderBy(GenerateSortQuery(sortingParameters))
+ : query;
+ }
+
+ ///
+ /// Applies multiple sets of sorting parameters to the query.
+ /// Returns the original query unchanged if no valid sorting parameters are provided.
+ ///
+ /// The element type of the query, constrained to .
+ /// The source queryable.
+ /// The collection of sorting parameters to apply in order.
+ /// The sorted (or original) queryable.
+ public static IQueryable ApplySorting(
+ this IQueryable query,
+ IEnumerable sortingParameters)
+ where T : IModelBase
+ {
+ var parametersSnapshot = sortingParameters?.Where(x => x.IsSortingSpecified)
+ .ToList() ?? Enumerable.Empty().ToList();
+
+ return parametersSnapshot.Count > 0
+ ? query.OrderBy(GenerateSortQuery(parametersSnapshot))
+ : query;
+ }
+
+ ///
+ /// Applies an optional decorator function to the query (e.g. for custom Include or Where clauses).
+ /// Returns the original query unchanged if is null.
+ ///
+ /// The element type of the query.
+ /// The source queryable.
+ /// An optional function that transforms the query.
+ /// The decorated (or original) queryable.
+ public static IQueryable AdditionOperation(this IQueryable query,
+ Func, IQueryable> decorator)
+ => decorator?.Invoke(query) ?? query;
+
+ private static string GenerateSortQuery(IEnumerable parameters)
+ {
+ return string.Join(",", parameters.Where(x => x.IsSortingSpecified)
+ .Select(GenerateSortQuery));
+ }
+
+ private static string GenerateSortQuery(ISortingParameters sortingParameter)
+ => sortingParameter.SortDir == SortDirection.Desc
+ ? $"{sortingParameter.SortField} {SortDirection.Desc}"
+ : sortingParameter.SortField;
+
+ }
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/AuditChangesCollector.cs b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/AuditChangesCollector.cs
index 67ab4f4..b09fa6b 100644
--- a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/AuditChangesCollector.cs
+++ b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/AuditChangesCollector.cs
@@ -1,7 +1,7 @@
-using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
+using Microsoft.EntityFrameworkCore;
using TailoredApps.Shared.EntityFramework.Interfaces.Audit;
using TailoredApps.Shared.EntityFramework.UnitOfWork.Audit.Extensions;
@@ -38,4 +38,4 @@ public IEnumerable CollectChanges()
.ToList();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/AuditEntityEntry.cs b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/AuditEntityEntry.cs
index 374ef8a..cd7565c 100644
--- a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/AuditEntityEntry.cs
+++ b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/AuditEntityEntry.cs
@@ -1,32 +1,71 @@
-using Microsoft.EntityFrameworkCore.ChangeTracking;
using System;
using System.Collections.Generic;
using System.Linq;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
using TailoredApps.Shared.EntityFramework.Interfaces.Audit;
using TailoredApps.Shared.EntityFramework.UnitOfWork.Audit.Extensions;
namespace TailoredApps.Shared.EntityFramework.UnitOfWork.Audit.Changes
{
+ ///
+ /// Represents the audit snapshot of a tracked entity entry captured at save time.
+ /// Provides access to the entity's current/original state, type, primary keys,
+ /// and the audit-specific entity state.
+ ///
internal interface IAuditEntityEntry
{
+ ///
+ /// Gets the audit-specific state of the entity (e.g. Added, Modified, Deleted).
+ ///
AuditEntityState EntityState { get; }
+ ///
+ /// Gets the entity object with its current (post-change) property values.
+ ///
object CurrentEntity { get; }
+ ///
+ /// Gets the entity object with its original (pre-change) property values.
+ ///
object OriginalEntity { get; }
+ ///
+ /// Gets the CLR type of the tracked entity.
+ ///
Type EntityType { get; }
+ ///
+ /// Returns a string identifier built from the entity type name and its primary key values.
+ ///
string GetPrimaryKeyStringIdentifier();
+
+ ///
+ /// Returns a dictionary mapping primary key property names to their current values.
+ ///
Dictionary GetPrimaryKeys();
+ ///
+ /// Copies primary key values from the EF Core tracked entity into both
+ /// and snapshots.
+ ///
void SetPrimaryKeys();
}
+ ///
+ /// Default implementation of that wraps an EF Core
+ /// and snapshots its current and original values at construction time.
+ ///
internal class AuditEntityEntry : IAuditEntityEntry
{
private readonly EntityEntry _entityEntry;
+ ///
+ /// Initializes a new instance of from an EF Core entity entry.
+ /// Snapshots , , ,
+ /// and at the time of construction.
+ ///
+ /// The EF Core change-tracker entry to wrap. Must not be null.
+ /// Thrown when is null.
public AuditEntityEntry(EntityEntry entityEntry)
{
_entityEntry = entityEntry ?? throw new ArgumentNullException(nameof(entityEntry));
@@ -36,17 +75,27 @@ public AuditEntityEntry(EntityEntry entityEntry)
EntityState = entityEntry.State.ToAuditEntityState();
}
+ ///
+ /// Creates a new from the given EF Core entity entry.
+ ///
+ /// The EF Core change-tracker entry to wrap.
+ /// A new instance.
public static IAuditEntityEntry Create(EntityEntry entityEntry)
=> new AuditEntityEntry(entityEntry);
+ ///
public AuditEntityState EntityState { get; }
+ ///
public object CurrentEntity { get; }
+ ///
public object OriginalEntity { get; }
+ ///
public Type EntityType { get; }
+ ///
public string GetPrimaryKeyStringIdentifier()
{
var primaryKeyValues = _entityEntry.Metadata.FindPrimaryKey()
@@ -55,6 +104,8 @@ public string GetPrimaryKeyStringIdentifier()
return $"{EntityType.Name}_{string.Join("_", primaryKeyValues)}";
}
+
+ ///
public Dictionary GetPrimaryKeys()
{
var primaryKey = _entityEntry.Metadata.FindPrimaryKey();
@@ -64,6 +115,7 @@ public Dictionary GetPrimaryKeys()
return keys;
}
+ ///
public void SetPrimaryKeys()
{
var primaryKeyProperties = _entityEntry.Metadata.FindPrimaryKey().Properties;
@@ -77,4 +129,4 @@ public void SetPrimaryKeys()
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/EntityChangeUpdateContext.cs b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/EntityChangeUpdateContext.cs
index b0925dc..44b37af 100644
--- a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/EntityChangeUpdateContext.cs
+++ b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/EntityChangeUpdateContext.cs
@@ -1,10 +1,30 @@
-using System;
+using System;
using System.Collections.Generic;
namespace TailoredApps.Shared.EntityFramework.UnitOfWork.Audit.Changes
{
+ ///
+ /// Carries all data required by an entity-change update operation:
+ /// the full changes dictionary, the identifier of the entity being updated,
+ /// the newly collected change, and the existing change record that will be mutated.
+ ///
internal class EntityChangeUpdateContext : IEntityChangeUpdateContext
{
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ /// The mutable dictionary that maps entity identifiers to their accumulated change records.
+ /// Must not be null and must already contain an entry for .
+ ///
+ /// The newly captured change to merge into the existing record.
+ /// The string key that uniquely identifies the entity within the dictionary.
+ ///
+ /// Thrown when any of the required arguments is null.
+ ///
+ ///
+ /// Thrown when does not contain .
+ ///
public EntityChangeUpdateContext(IDictionary entityChangesDictionary,
IInternalEntityChange collectedEntityChange, string identifier)
{
@@ -18,17 +38,43 @@ public EntityChangeUpdateContext(IDictionary enti
ExistingEntityChange = existingEntityChange;
}
+ ///
public IDictionary EntityChangesDictionary { get; }
+
+ ///
public string Identifier { get; }
+
+ ///
public IInternalEntityChange CollectedEntityChange { get; }
+
+ ///
public IInternalEntityChange ExistingEntityChange { get; }
}
+ ///
+ /// Defines the data contract for an entity-change update context used by
+ /// to resolve and execute the correct merge strategy.
+ ///
internal interface IEntityChangeUpdateContext
{
+ ///
+ /// Gets the dictionary of accumulated entity change records, keyed by entity identifier.
+ ///
IDictionary EntityChangesDictionary { get; }
+
+ ///
+ /// Gets the string identifier that uniquely identifies the entity within .
+ ///
string Identifier { get; }
+
+ ///
+ /// Gets the newly collected entity change that should be merged into .
+ ///
IInternalEntityChange CollectedEntityChange { get; }
+
+ ///
+ /// Gets the existing entity change record stored in that will be updated.
+ ///
IInternalEntityChange ExistingEntityChange { get; }
}
-}
\ No newline at end of file
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/EntityChangeUpdateOperationFactory.cs b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/EntityChangeUpdateOperationFactory.cs
index d55a3df..0dab8e9 100644
--- a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/EntityChangeUpdateOperationFactory.cs
+++ b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/EntityChangeUpdateOperationFactory.cs
@@ -1,9 +1,15 @@
-using System;
+using System;
using System.Collections.Generic;
using TailoredApps.Shared.EntityFramework.Interfaces.Audit;
namespace TailoredApps.Shared.EntityFramework.UnitOfWork.Audit.Changes
{
+ ///
+ /// Factory that maps an to the appropriate
+ /// entity-change update operation. Each operation defines how an existing audit record
+ /// should be mutated when an entity transitions between two values
+ /// within the same transaction.
+ ///
internal static class EntityChangeUpdateOperationFactory
{
private static readonly IDictionary> UpdateOperations =
@@ -55,6 +61,19 @@ internal static class EntityChangeUpdateOperationFactory
},
};
+ ///
+ /// Returns the update operation delegate for the given .
+ ///
+ ///
+ /// The transition describing the previous and new audit state of the entity.
+ ///
+ ///
+ /// An that applies the correct merge
+ /// strategy to the existing audit change record.
+ ///
+ ///
+ /// Thrown when no operation is registered for the supplied .
+ ///
public static Action Create(EntityStateTransition entityStateTransition)
{
if (UpdateOperations.TryGetValue(entityStateTransition, out var updateOperation))
@@ -63,4 +82,4 @@ public static Action Create(EntityStateTransition en
throw new InvalidOperationException("Unexpected entity state transition within current transaction.");
}
}
-}
\ No newline at end of file
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/EntityStateTransition.cs b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/EntityStateTransition.cs
index 34b6542..12c64e7 100644
--- a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/EntityStateTransition.cs
+++ b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/EntityStateTransition.cs
@@ -1,4 +1,4 @@
-using TailoredApps.Shared.EntityFramework.Interfaces.Audit;
+using TailoredApps.Shared.EntityFramework.Interfaces.Audit;
namespace TailoredApps.Shared.EntityFramework.UnitOfWork.Audit.Changes
{
@@ -33,4 +33,4 @@ public override bool Equals(object obj)
return From == otherStateTransition.From && To == otherStateTransition.To;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/InternalEntityChange.cs b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/InternalEntityChange.cs
index ee7a52c..977a6d1 100644
--- a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/InternalEntityChange.cs
+++ b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Changes/InternalEntityChange.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using TailoredApps.Shared.EntityFramework.Interfaces.Audit;
@@ -39,4 +39,4 @@ public object GetOriginalEntity()
public AuditEntityState GetAuditEntityState()
=> State;
}
-}
\ No newline at end of file
+}
diff --git a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Configuration/AuditSettings.cs b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Configuration/AuditSettings.cs
index 8ab7a04..56d6333 100644
--- a/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Configuration/AuditSettings.cs
+++ b/src/TailoredApps.Shared.EntityFramework/UnitOfWork/Audit/Configuration/AuditSettings.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using TailoredApps.Shared.EntityFramework.Interfaces.Audit;
@@ -10,4 +10,4 @@ internal sealed class AuditSettings : IAuditSettings
public IEnumerable TypesToCollect { get; set; } = Enumerable.Empty();
public IEnumerable EntityStatesToCollect { get; set; } = Enumerable.Empty