From ec956d88426475413d1c7e89a6404407408d1fee Mon Sep 17 00:00:00 2001 From: Robert McLaws <1657085+robertmclaws@users.noreply.github.com> Date: Sat, 1 Aug 2026 20:55:54 -0400 Subject: [PATCH 1/9] Content fixes Escaping problems. --- .../guides/business-layer.mdx | 28 +++++++++---------- .../guides/data-operations-and-audit.mdx | 16 +++++------ .../guides/observable-objects.mdx | 8 +++--- .../guides/state-machines-and-status.mdx | 4 +-- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/CloudNimble.EasyAF.Docs/guides/business-layer.mdx b/src/CloudNimble.EasyAF.Docs/guides/business-layer.mdx index fb9c53c..8a0ccec 100644 --- a/src/CloudNimble.EasyAF.Docs/guides/business-layer.mdx +++ b/src/CloudNimble.EasyAF.Docs/guides/business-layer.mdx @@ -91,10 +91,10 @@ public class ProductManager : EntityManager ### Audit Field Population Automatically handles interfaces: -- **ICreatorTrackable**: Sets CreatedById from ClaimsPrincipal.Current -- **ICreatedAuditable**: Sets DateCreated to DateTime.UtcNow -- **IUpdaterTrackable**: Sets UpdatedById from ClaimsPrincipal.Current -- **IUpdatedAuditable**: Sets DateUpdated to DateTime.UtcNow +- **`ICreatorTrackable`**: Sets CreatedById from ClaimsPrincipal.Current +- **`ICreatedAuditable`**: Sets DateCreated to DateTime.UtcNow +- **`IUpdaterTrackable`**: Sets UpdatedById from ClaimsPrincipal.Current +- **`IUpdatedAuditable`**: Sets DateUpdated to DateTime.UtcNow ### CRUD Operations @@ -145,7 +145,7 @@ await manager.DirectDeleteAsync(e => e.IsDeleted == true); ## IdentifiableEntityManager -Extends EntityManager for entities with ID properties (implementing IIdentifiable). +Extends EntityManager for entities with ID properties (implementing `IIdentifiable`). ### Key Feature - **Automatic GUID Generation**: Creates new GUIDs for entities with empty IDs during insertion @@ -167,7 +167,7 @@ public class OrderManager : IdentifiableEntityManager ## StatusEntityManager -Manages entities with status tracking (implementing IHasStatus). +Manages entities with status tracking (implementing `IHasStatus`). ### Features - **Status Type Management**: Loads and caches available status types @@ -202,7 +202,7 @@ public class InvoiceManager : StatusEntityManager). +Manages entities with state machine workflows (implementing `IHasState`). ### Features - **State Type Management**: Loads and caches available state types @@ -272,13 +272,13 @@ public class WorkflowManager : StateMachineEntityManager**: Entity has an ID property -- **ICreatedAuditable**: Track creation timestamp -- **IUpdatedAuditable**: Track update timestamp -- **ICreatorTrackable**: Track user who created -- **IUpdaterTrackable**: Track user who updated -- **IHasStatus**: Entity has status type -- **IHasState**: Entity participates in state machine +- **`IIdentifiable`**: Entity has an ID property +- **`ICreatedAuditable`**: Track creation timestamp +- **`IUpdatedAuditable`**: Track update timestamp +- **`ICreatorTrackable`**: Track user who created +- **`IUpdaterTrackable`**: Track user who updated +- **`IHasStatus`**: Entity has status type +- **`IHasState`**: Entity participates in state machine ## Thread Safety diff --git a/src/CloudNimble.EasyAF.Docs/guides/data-operations-and-audit.mdx b/src/CloudNimble.EasyAF.Docs/guides/data-operations-and-audit.mdx index 9e04d3b..d0aeaea 100644 --- a/src/CloudNimble.EasyAF.Docs/guides/data-operations-and-audit.mdx +++ b/src/CloudNimble.EasyAF.Docs/guides/data-operations-and-audit.mdx @@ -15,7 +15,7 @@ EasyAF provides comprehensive data operation management with automatic audit tra ### Identity Management -#### IIdentifiable +#### `IIdentifiable` Ensures entities have a unique identifier. ```csharp @@ -53,7 +53,7 @@ public interface IUpdatedAuditable ### User Tracking Interfaces -#### ICreatorTrackable +#### `ICreatorTrackable` Tracks which user created an entity. ```csharp @@ -63,7 +63,7 @@ public interface ICreatorTrackable where T : struct } ``` -#### IUpdaterTrackable +#### `IUpdaterTrackable` Tracks which user last updated an entity. ```csharp @@ -157,9 +157,9 @@ public class AuditableEntity : DbObservableObject, 1. **Client creates entity** → New instance with business data 2. **Manager.InsertAsync() called** → Initiates insert operation 3. **OnInsertingAsync() executed** → - - ID generated (if IIdentifiable) - - CreatedById set (if ICreatorTrackable) - - DateCreated set (if ICreatedAuditable) + - ID generated (if `IIdentifiable`) + - CreatedById set (if `ICreatorTrackable`) + - DateCreated set (if `ICreatedAuditable`) 4. **Entity added to context** → EntityState.Added 5. **SaveChangesAsync()** → Database insert 6. **OnInsertedAsync() executed** → Post-insert logic (events, notifications) @@ -169,8 +169,8 @@ public class AuditableEntity : DbObservableObject, 1. **Entity retrieved and modified** → Property changes tracked 2. **Manager.UpdateAsync() called** → Initiates update operation 3. **OnUpdatingAsync() executed** → - - UpdatedById set (if IUpdaterTrackable) - - DateUpdated set (if IUpdatedAuditable) + - UpdatedById set (if `IUpdaterTrackable`) + - DateUpdated set (if `IUpdatedAuditable`) 4. **Entity marked modified** → EntityState.Modified 5. **SaveChangesAsync()** → Database update 6. **OnUpdatedAsync() executed** → Post-update logic diff --git a/src/CloudNimble.EasyAF.Docs/guides/observable-objects.mdx b/src/CloudNimble.EasyAF.Docs/guides/observable-objects.mdx index 15b1333..64a1236 100644 --- a/src/CloudNimble.EasyAF.Docs/guides/observable-objects.mdx +++ b/src/CloudNimble.EasyAF.Docs/guides/observable-objects.mdx @@ -46,10 +46,10 @@ public class Person : EasyObservableObject ### Methods -- **Set(propertyName, ref field, newValue)**: Sets a property value and raises PropertyChanged if the value changes -- **Set(propertyExpression, ref field, newValue)**: Expression-based property setter for compile-time safety -- **RaisePropertyChanged(propertyName)**: Manually raises the PropertyChanged event -- **Clone()**: Creates a deep copy of the object using JSON serialization +- **`Set(propertyName, ref field, newValue)`**: Sets a property value and raises PropertyChanged if the value changes +- **`Set(propertyExpression, ref field, newValue)`**: Expression-based property setter for compile-time safety +- **`RaisePropertyChanged(propertyName)`**: Manually raises the PropertyChanged event +- **`Clone()`**: Creates a deep copy of the object using JSON serialization ## DbObservableObject diff --git a/src/CloudNimble.EasyAF.Docs/guides/state-machines-and-status.mdx b/src/CloudNimble.EasyAF.Docs/guides/state-machines-and-status.mdx index fded2e3..c7beafc 100644 --- a/src/CloudNimble.EasyAF.Docs/guides/state-machines-and-status.mdx +++ b/src/CloudNimble.EasyAF.Docs/guides/state-machines-and-status.mdx @@ -83,7 +83,7 @@ public interface IDbStatusEnum : IDbEnum } ``` -#### IHasStatus +#### `IHasStatus` Entities with status: ```csharp public interface IHasStatus : IIdentifiable @@ -114,7 +114,7 @@ public interface IDbStateEnum : IDbEnum } ``` -#### IHasState +#### `IHasState` Entities in state machine: ```csharp public interface IHasState : IIdentifiable From 27d17caaa9e684f097055389d626dd5f514e44fc Mon Sep 17 00:00:00 2001 From: Robert McLaws <1657085+robertmclaws@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:56:37 -0400 Subject: [PATCH 2/9] Update docs.json --- src/CloudNimble.EasyAF.Docs/docs.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/CloudNimble.EasyAF.Docs/docs.json b/src/CloudNimble.EasyAF.Docs/docs.json index 0446455..8c56f50 100644 --- a/src/CloudNimble.EasyAF.Docs/docs.json +++ b/src/CloudNimble.EasyAF.Docs/docs.json @@ -42,8 +42,7 @@ "guides/state-machines-and-status", "guides/interval-calculations", "guides/property-name-overrides", - "guides/MSBuild/dotnet-10-changes", - "guides/clarification-needed" + "guides/MSBuild/dotnet-10-changes" ] }, { From d08cd1ed3da1443aef66449ad3be632103235517 Mon Sep 17 00:00:00 2001 From: Robert McLaws <1657085+robertmclaws@users.noreply.github.com> Date: Mon, 14 Sep 2026 04:05:14 -0400 Subject: [PATCH 3/9] Add net11 support, CSDL LongDescription, and isolate EF tests. - Target net11.0 (SDK 11 RC1); keep per-TFM package groups with 12.0.0 caps; drop coverlet; floor test STJ at 10.x. - Map CSDL Summary to and LongDescription to ; skip empty XML docs. - Load SQL Server EasyAF_LongDescription extended properties into CSDL LongDescription. - Stub DirectUpdate/DirectDelete on EF Core net11 (Z.EntityFramework.Plus has no 11.x package). - Round Interval test expectations to 8 decimals for net11 Convert.ToDecimal(double). - Stop CodeGen tests from writing Baselines; isolate EntityManager tests from shared LocalDB. --- global.json | 5 + .../CloudNimble.EasyAF.Business.EFCore.csproj | 38 +-- .../CloudNimble.EasyAF.Business.EF6.csproj | 19 +- .../EntityManager.cs | 20 ++ .../CloudNimble.EasyAF.CodeGen.csproj | 10 +- .../Generators/Base/CodeGeneratorBase.cs | 44 ++- .../Core/DbContextPartialGenerator.cs | 6 +- .../Generators/Core/EntityGenerator.cs | 16 +- .../Legacy/MetadataTools.cs | 144 +++++----- .../CloudNimble.EasyAF.Configuration.csproj | 23 +- .../CloudNimble.EasyAF.Core.csproj | 16 +- .../CloudNimble.EasyAF.Data.EF6.csproj | 4 +- .../CloudNimble.EasyAF.Data.EFCore.csproj | 14 +- src/CloudNimble.EasyAF.Docs/assembly-list.txt | 34 +-- .../CloudNimble.EasyAF.EFCoreToEdmx.csproj | 14 +- .../EasyAFLongDescription.cs | 229 +++++++++++++++ .../EdmxConverter.cs | 20 +- .../EdmxXmlGenerator.cs | 51 +++- .../Models/EdmxEntityType.cs | 7 +- .../Models/EdmxProperty.cs | 7 +- ...udNimble.EasyAF.Http.NewtonsoftJson.csproj | 4 +- ...udNimble.EasyAF.Http.SystemTextJson.csproj | 2 +- .../CloudNimble.EasyAF.Http.csproj | 41 +-- .../CloudNimble.EasyAF.MSBuild.csproj | 21 +- ...EasyAF.NewtonsoftJson.Compatibility.csproj | 4 +- .../CloudNimble.EasyAF.ODataClient.csproj | 13 +- ...oudNimble.EasyAF.Restier.Breakdance.csproj | 12 +- .../CloudNimble.EasyAF.Restier.EF6.csproj | 19 +- .../CloudNimble.EasyAF.Restier.EFCore.csproj | 19 +- .../CloudNimble.EasyAF.Restier.csproj | 2 +- ...oudNimble.EasyAF.Tests.Business.EF6.csproj | 12 +- .../EntityManagerTests.cs | 156 +++++++---- .../DbContexts/EasyAFEntities.Generated.cs | 20 +- .../Baselines/Entities/Inquiry.Generated.cs | 38 +-- .../Baselines/Entities/Product.Generated.cs | 32 +-- .../Baselines/Entities/User.Generated.cs | 26 +- .../DocumentationDiagnosticTests.cs | 260 ++++++++++++++++-- .../SimpleMessageBusGeneratorTests.cs | 39 --- ...udNimble.EasyAF.Tests.Configuration.csproj | 14 +- .../CloudNimble.EasyAF.Tests.Core.csproj | 2 +- .../IntervalTests.cs | 12 +- .../CloudNimble.EasyAF.Tests.Data.EF6.csproj | 2 +- .../EntityFramework6Tests.cs | 1 + ...oudNimble.EasyAF.Tests.EFCoreToEdmx.csproj | 10 +- .../EasyAFLongDescriptionTests.cs | 180 ++++++++++++ .../EdmxXmlGeneratorTests.cs | 53 ++++ ...le.EasyAF.Tests.Http.NewtonsoftJson.csproj | 9 +- ...le.EasyAF.Tests.Http.SystemTextJson.csproj | 12 +- .../CloudNimble.EasyAF.Tests.Http.csproj | 2 +- .../CloudNimble.EasyAF.Tests.MSBuild.csproj | 2 +- .../ApiClientTests.cs | 2 +- ...loudNimble.EasyAF.Tests.ODataClient.csproj | 17 +- .../CloudNimble.EasyAF.Tests.Restier.csproj | 7 +- .../CloudNimble.EasyAF.Tests.Shared.csproj | 2 +- .../CloudNimble.EasyAF.Tests.Tools.csproj | 2 +- .../CloudNimble.EasyAF.Tools.csproj | 8 +- ...CloudNimble.EasyAF.XmlDocumentation.csproj | 2 +- src/Directory.Build.props | 12 +- src/global.json | 7 +- 59 files changed, 1283 insertions(+), 516 deletions(-) create mode 100644 src/CloudNimble.EasyAF.EFCoreToEdmx/EasyAFLongDescription.cs create mode 100644 src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/EasyAFLongDescriptionTests.cs diff --git a/global.json b/global.json index 3140116..62939fe 100644 --- a/global.json +++ b/global.json @@ -1,4 +1,9 @@ { + "sdk": { + "version": "11.0.100-rc.1.26425.128", + "rollForward": "latestFeature", + "allowPrerelease": true + }, "test": { "runner": "Microsoft.Testing.Platform" } diff --git a/src/CloudNimble.EasyAF.Business.EFCore/CloudNimble.EasyAF.Business.EFCore.csproj b/src/CloudNimble.EasyAF.Business.EFCore/CloudNimble.EasyAF.Business.EFCore.csproj index 3258b97..09ba708 100644 --- a/src/CloudNimble.EasyAF.Business.EFCore/CloudNimble.EasyAF.Business.EFCore.csproj +++ b/src/CloudNimble.EasyAF.Business.EFCore/CloudNimble.EasyAF.Business.EFCore.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; $(DocumentationFile)\$(AssemblyName).xml EFCORE @@ -11,37 +11,43 @@ + + + + + + - - - + + + - - - + + + - + - - - + + + - + - - - + + + - + diff --git a/src/CloudNimble.EasyAF.Business/CloudNimble.EasyAF.Business.EF6.csproj b/src/CloudNimble.EasyAF.Business/CloudNimble.EasyAF.Business.EF6.csproj index 64805c5..df9949b 100644 --- a/src/CloudNimble.EasyAF.Business/CloudNimble.EasyAF.Business.EF6.csproj +++ b/src/CloudNimble.EasyAF.Business/CloudNimble.EasyAF.Business.EF6.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; $(DocumentationFile)\$(AssemblyName).xml @@ -12,22 +12,27 @@ + + + + + - + - + - + - + - + - + diff --git a/src/CloudNimble.EasyAF.Business/EntityManager.cs b/src/CloudNimble.EasyAF.Business/EntityManager.cs index bcbe8cd..6e05873 100644 --- a/src/CloudNimble.EasyAF.Business/EntityManager.cs +++ b/src/CloudNimble.EasyAF.Business/EntityManager.cs @@ -459,7 +459,12 @@ public async Task DirectUpdateAsync(Expression> predica { Ensure.ArgumentNotNull(predicate, nameof(predicate)); +#if NET11_0 && EFCORE + Console.WriteLine("DirectUpdateAsync is not currently available on .NET 11."); + return await Task.FromResult(0).ConfigureAwait(false); +#else return await DataContext.Set().Where(predicate).UpdateFromQueryAsync(updateExpression).ConfigureAwait(false); +#endif } /// @@ -476,7 +481,12 @@ public int DirectUpdate(Expression> predicate, Expression().Where(predicate).UpdateFromQuery(updateExpression); +#endif } #endregion @@ -566,7 +576,12 @@ public async Task DirectDeleteAsync(Expression> predicat { Ensure.ArgumentNotNull(predicate, nameof(predicate)); +#if NET11_0 && EFCORE + Console.WriteLine("DirectDeleteAsync is not currently available on .NET 11."); + return await Task.FromResult(0).ConfigureAwait(false); +#else return await DataContext.Set().Where(predicate).DeleteFromQueryAsync().ConfigureAwait(false); +#endif } /// @@ -582,7 +597,12 @@ public int DirectDelete(Expression> predicate) { Ensure.ArgumentNotNull(predicate, nameof(predicate)); +#if NET11_0 && EFCORE + Console.WriteLine("DirectDelete is not currently available on .NET 11."); + return 0; +#else return DataContext.Set().Where(predicate).DeleteFromQuery(); +#endif } #endregion diff --git a/src/CloudNimble.EasyAF.CodeGen/CloudNimble.EasyAF.CodeGen.csproj b/src/CloudNimble.EasyAF.CodeGen/CloudNimble.EasyAF.CodeGen.csproj index 3efbac6..509872c 100644 --- a/src/CloudNimble.EasyAF.CodeGen/CloudNimble.EasyAF.CodeGen.csproj +++ b/src/CloudNimble.EasyAF.CodeGen/CloudNimble.EasyAF.CodeGen.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0;netstandard2.0; + net11.0;net10.0;net9.0;net8.0;netstandard2.0; true $(DocumentationFile)\$(AssemblyName).xml $(NoWarn);CA1822;CS8002;NU1701;NU1608; @@ -11,6 +11,14 @@ + + + + + + + + diff --git a/src/CloudNimble.EasyAF.CodeGen/Generators/Base/CodeGeneratorBase.cs b/src/CloudNimble.EasyAF.CodeGen/Generators/Base/CodeGeneratorBase.cs index a3e4b07..0fe6437 100644 --- a/src/CloudNimble.EasyAF.CodeGen/Generators/Base/CodeGeneratorBase.cs +++ b/src/CloudNimble.EasyAF.CodeGen/Generators/Base/CodeGeneratorBase.cs @@ -1,4 +1,5 @@ -using System; +using CloudNimble.EasyAF.CodeGen.Legacy; +using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.IO; @@ -74,10 +75,10 @@ public CodeGeneratorBase(List extraUsings, string namespaceName) #region Public Methods /// - /// Sets the Indent to 1, writes the Summary tag, the Class declaration, and then the opening bracket. + /// Sets the Indent to 1, writes XML docs when present, the Class declaration, and then the opening bracket. /// /// The full Class declaration string. - /// The test to put inside the <summary> tag. + /// The text to put inside the <summary> tag. public void ClassBegin(string declaration, string summaryText) { _writer.Indent = 1; @@ -87,6 +88,43 @@ public void ClassBegin(string declaration, string summaryText) SectionBegin(declaration, 1); } + /// + /// Sets the Indent to 1, writes XML docs when present, the Class declaration, and then the opening bracket. + /// + /// The full Class declaration string. + /// CSDL documentation mapped to summary / remarks. Empty docs emit no XML comment block. + public void ClassBegin(string declaration, XmlDocComment docs) + { + _writer.Indent = 1; + WriteXmlDocs(docs); + SectionBegin(declaration, 1); + } + + /// + /// Writes C# XML documentation tags for the supplied comment. Emits nothing when both fields are empty. + /// + protected void WriteXmlDocs(XmlDocComment docs) + { + if (docs.IsEmpty) + { + return; + } + + if (docs.HasSummary) + { + _writer.WriteLine("/// "); + _writer.WriteLine($"/// {docs.Summary}"); + _writer.WriteLine("/// "); + } + + if (docs.HasRemarks) + { + _writer.WriteLine("/// "); + _writer.WriteLine($"/// {docs.Remarks}"); + _writer.WriteLine("/// "); + } + } + /// /// Writes the end of a Class. /// diff --git a/src/CloudNimble.EasyAF.CodeGen/Generators/Core/DbContextPartialGenerator.cs b/src/CloudNimble.EasyAF.CodeGen/Generators/Core/DbContextPartialGenerator.cs index 71ef6ed..c2e5511 100644 --- a/src/CloudNimble.EasyAF.CodeGen/Generators/Core/DbContextPartialGenerator.cs +++ b/src/CloudNimble.EasyAF.CodeGen/Generators/Core/DbContextPartialGenerator.cs @@ -60,7 +60,7 @@ public override void Generate() Header(); WriteUsings(); NamespaceBegin(Namespace); - ClassBegin(CodeGenerationTools.DbContextClassDeclaration(EntityContainer), MetadataTools.Comment(EntityContainer)); + ClassBegin(CodeGenerationTools.DbContextClassDeclaration(EntityContainer), MetadataTools.ToXmlDoc(EntityContainer)); WriteProperties(); WriteConstructors(); WriteOverrides(); @@ -194,9 +194,7 @@ internal void WriteProperties() RegionBegin("Public Properties"); foreach (var entitySet in EntityContainer.BaseEntitySets.OfType().OrderBy(c => c.Name)) { - _writer.WriteLine("/// "); - _writer.WriteLine($"/// {MetadataTools.Comment(entitySet)}"); - _writer.WriteLine("/// "); + WriteXmlDocs(MetadataTools.ToXmlDoc(entitySet)); _writer.WriteLine(CodeGenerationTools.DbSet(entitySet)); _writer.WriteLine(); } diff --git a/src/CloudNimble.EasyAF.CodeGen/Generators/Core/EntityGenerator.cs b/src/CloudNimble.EasyAF.CodeGen/Generators/Core/EntityGenerator.cs index e2372a1..ec0ef68 100644 --- a/src/CloudNimble.EasyAF.CodeGen/Generators/Core/EntityGenerator.cs +++ b/src/CloudNimble.EasyAF.CodeGen/Generators/Core/EntityGenerator.cs @@ -39,7 +39,7 @@ public override void Generate() Header(); WriteUsings(); NamespaceBegin(Namespace); - ClassBegin(CodeGenerationTools.EntityClassDeclaration(Entity), MetadataTools.Comment(Entity.EntityType)); + ClassBegin(CodeGenerationTools.EntityClassDeclaration(Entity), MetadataTools.ToXmlDoc(Entity.EntityType)); WriteFields(); WriteProperties(); WriteConstructors(); @@ -65,9 +65,7 @@ internal void WriteConstructors() { RegionBegin("Constructors"); - _writer.WriteLine("/// "); - _writer.WriteLine($"/// {MetadataTools.Comment(Entity.EntityType)}"); - _writer.WriteLine("/// "); + WriteXmlDocs(MetadataTools.ToXmlDoc(Entity.EntityType)); _writer.WriteLine($"public {CodeGenerationTools.Escape(Entity.EntityType)}()"); _writer.WriteLine("{"); _writer.WriteLine("}"); @@ -116,7 +114,7 @@ internal void WriteProperty(EdmProperty property) CodeGenerationTools.Escape(property), CodeGenerationTools.FieldName(property), CodeGenerationTools.GetTypeName(property.TypeUsage).Replace("System.", ""), - MetadataTools.Comment(property), + MetadataTools.ToXmlDoc(property), property.TypeUsage.Facets.ToList()); } @@ -126,15 +124,13 @@ internal void WriteProperty(NavigationProperty property) CodeGenerationTools.Escape(property), CodeGenerationTools.FieldName(property), CodeGenerationTools.GetTypeName(property.TypeUsage).Replace("System.", ""), - MetadataTools.Comment(property), + MetadataTools.ToXmlDoc(property), property.TypeUsage.Facets.ToList()); } - internal void WriteProperty(string accessibility, string propertyName, string fieldName, string type, string comment, List facets) + internal void WriteProperty(string accessibility, string propertyName, string fieldName, string type, XmlDocComment docs, List facets) { - _writer.WriteLine("/// "); - _writer.WriteLine($"/// {comment}"); - _writer.WriteLine("/// "); + WriteXmlDocs(docs); var facet = facets.FirstOrDefault(c => c.Name == "MaxLength" && c.Value is not null && c.IsUnbounded == false); if (facet is not null) diff --git a/src/CloudNimble.EasyAF.CodeGen/Legacy/MetadataTools.cs b/src/CloudNimble.EasyAF.CodeGen/Legacy/MetadataTools.cs index af961a3..b9e57c7 100644 --- a/src/CloudNimble.EasyAF.CodeGen/Legacy/MetadataTools.cs +++ b/src/CloudNimble.EasyAF.CodeGen/Legacy/MetadataTools.cs @@ -7,6 +7,49 @@ namespace CloudNimble.EasyAF.CodeGen.Legacy { + + /// + /// CSDL documentation mapped onto C# XML doc tags: Summary → summary, LongDescription → remarks. + /// + public readonly struct XmlDocComment + { + /// + /// Initializes a new . + /// + /// The summary text. + /// The remarks text. + public XmlDocComment(string summary, string remarks) + { + Summary = summary; + Remarks = remarks; + } + + /// + /// Gets the summary text. + /// + public string Summary { get; } + + /// + /// Gets the remarks text. + /// + public string Remarks { get; } + + /// + /// Gets a value indicating whether is present. + /// + public bool HasSummary => !string.IsNullOrEmpty(Summary); + + /// + /// Gets a value indicating whether is present. + /// + public bool HasRemarks => !string.IsNullOrEmpty(Remarks); + + /// + /// Gets a value indicating whether both fields are empty. + /// + public bool IsEmpty => !HasSummary && !HasRemarks; + } + /// /// Responsible for making the Entity Framework Metadata more accessible for code generation. /// @@ -26,103 +69,64 @@ public static Type ClrType(TypeUsage typeUsage) } /// - /// Gets the documentation comment for an EDM type. + /// Maps CSDL documentation onto C# XML doc tags. Summary → summary, LongDescription → remarks. + /// Empty strings are treated as missing. LongDescription is never copied into Summary. + /// + public static XmlDocComment ToXmlDoc(Documentation documentation) + { + if (documentation is null) + { + return default; + } + + var summary = string.IsNullOrEmpty(documentation.Summary) ? null : SanitizeXmlComment(documentation.Summary); + var remarks = string.IsNullOrEmpty(documentation.LongDescription) ? null : SanitizeXmlComment(documentation.LongDescription); + return new XmlDocComment(summary, remarks); + } + + /// + /// Gets the XML documentation for an EDM type. /// - /// The EDM type. - /// The documentation comment, preferring LongDescription over Summary. - public static string Comment(EdmType edmType) + public static XmlDocComment ToXmlDoc(EdmType edmType) { Ensure.ArgumentNotNull(edmType, nameof(edmType)); - var doc = edmType.Documentation; - if (doc is null) return string.Empty; - - // Prefer LongDescription over Summary, but check for empty strings - // since Documentation properties default to "" not null - if (!string.IsNullOrEmpty(doc.LongDescription)) - return SanitizeXmlComment(doc.LongDescription); - if (!string.IsNullOrEmpty(doc.Summary)) - return SanitizeXmlComment(doc.Summary); - return string.Empty; + return ToXmlDoc(edmType.Documentation); } /// - /// Gets the documentation comment for an EDM property. + /// Gets the XML documentation for an EDM property. /// - /// The EDM property. - /// The documentation comment, preferring LongDescription over Summary. - public static string Comment(EdmProperty edmProperty) + public static XmlDocComment ToXmlDoc(EdmProperty edmProperty) { Ensure.ArgumentNotNull(edmProperty, nameof(edmProperty)); - var doc = edmProperty.Documentation; - if (doc is null) return string.Empty; - - // Prefer LongDescription over Summary, but check for empty strings - // since Documentation properties default to "" not null - if (!string.IsNullOrEmpty(doc.LongDescription)) - return SanitizeXmlComment(doc.LongDescription); - if (!string.IsNullOrEmpty(doc.Summary)) - return SanitizeXmlComment(doc.Summary); - return string.Empty; + return ToXmlDoc(edmProperty.Documentation); } /// - /// Gets the documentation comment for a navigation property. + /// Gets the XML documentation for a navigation property. /// - /// The navigation property. - /// The documentation comment, preferring LongDescription over Summary. - public static string Comment(NavigationProperty navigationProperty) + public static XmlDocComment ToXmlDoc(NavigationProperty navigationProperty) { Ensure.ArgumentNotNull(navigationProperty, nameof(navigationProperty)); - var doc = navigationProperty.Documentation; - if (doc is null) return string.Empty; - - // Prefer LongDescription over Summary, but check for empty strings - // since Documentation properties default to "" not null - if (!string.IsNullOrEmpty(doc.LongDescription)) - return SanitizeXmlComment(doc.LongDescription); - if (!string.IsNullOrEmpty(doc.Summary)) - return SanitizeXmlComment(doc.Summary); - return string.Empty; + return ToXmlDoc(navigationProperty.Documentation); } /// - /// Gets the documentation comment for an entity container. + /// Gets the XML documentation for an entity container. /// - /// The entity container. - /// The documentation comment, preferring LongDescription over Summary. - public static string Comment(EntityContainer container) + public static XmlDocComment ToXmlDoc(EntityContainer container) { Ensure.ArgumentNotNull(container, nameof(container)); - var doc = container.Documentation; - if (doc is null) return string.Empty; - - // Prefer LongDescription over Summary, but check for empty strings - // since Documentation properties default to "" not null - if (!string.IsNullOrEmpty(doc.LongDescription)) - return SanitizeXmlComment(doc.LongDescription); - if (!string.IsNullOrEmpty(doc.Summary)) - return SanitizeXmlComment(doc.Summary); - return string.Empty; + return ToXmlDoc(container.Documentation); } /// - /// Gets the documentation comment for an entity set. + /// Gets the XML documentation for an entity set. /// - /// The entity set. - /// The documentation comment, preferring LongDescription over Summary. - public static string Comment(EntitySet entitySet) + public static XmlDocComment ToXmlDoc(EntitySet entitySet) { Ensure.ArgumentNotNull(entitySet, nameof(entitySet)); - var doc = entitySet.Documentation; - if (doc is null) return string.Empty; - - // Prefer LongDescription over Summary, but check for empty strings - // since Documentation properties default to "" not null - if (!string.IsNullOrEmpty(doc.LongDescription)) - return SanitizeXmlComment(doc.LongDescription); - if (!string.IsNullOrEmpty(doc.Summary)) - return SanitizeXmlComment(doc.Summary); - return string.Empty; + return ToXmlDoc(entitySet.Documentation); } /// diff --git a/src/CloudNimble.EasyAF.Configuration/CloudNimble.EasyAF.Configuration.csproj b/src/CloudNimble.EasyAF.Configuration/CloudNimble.EasyAF.Configuration.csproj index 82acf72..77cf438 100644 --- a/src/CloudNimble.EasyAF.Configuration/CloudNimble.EasyAF.Configuration.csproj +++ b/src/CloudNimble.EasyAF.Configuration/CloudNimble.EasyAF.Configuration.csproj @@ -1,28 +1,33 @@  - net10.0;net9.0;net8.0;netstandard2.0; + net11.0;net10.0;net9.0;net8.0;netstandard2.0; $(DocumentationFile)\$(AssemblyName).xml + + + + + - - + + - - + + - - + + - - + + diff --git a/src/CloudNimble.EasyAF.Core/CloudNimble.EasyAF.Core.csproj b/src/CloudNimble.EasyAF.Core/CloudNimble.EasyAF.Core.csproj index 9a8c4b8..e711f0d 100644 --- a/src/CloudNimble.EasyAF.Core/CloudNimble.EasyAF.Core.csproj +++ b/src/CloudNimble.EasyAF.Core/CloudNimble.EasyAF.Core.csproj @@ -1,30 +1,34 @@  - net10.0;net9.0;net8.0;netstandard2.0; + net11.0;net10.0;net9.0;net8.0;netstandard2.0; $(DocumentationFile)\$(AssemblyName).xml - + + + + + - + - + - + - + diff --git a/src/CloudNimble.EasyAF.Data.EF6/CloudNimble.EasyAF.Data.EF6.csproj b/src/CloudNimble.EasyAF.Data.EF6/CloudNimble.EasyAF.Data.EF6.csproj index 1ea4a6b..4163093 100644 --- a/src/CloudNimble.EasyAF.Data.EF6/CloudNimble.EasyAF.Data.EF6.csproj +++ b/src/CloudNimble.EasyAF.Data.EF6/CloudNimble.EasyAF.Data.EF6.csproj @@ -2,7 +2,7 @@ - net48;net10.0;net9.0;net8.0; + net48;net11.0;net10.0;net9.0;net8.0; $(DocumentationFile)\$(AssemblyName).xml @@ -11,7 +11,7 @@ - + diff --git a/src/CloudNimble.EasyAF.Data.EFCore/CloudNimble.EasyAF.Data.EFCore.csproj b/src/CloudNimble.EasyAF.Data.EFCore/CloudNimble.EasyAF.Data.EFCore.csproj index d07ded8..4d8faab 100644 --- a/src/CloudNimble.EasyAF.Data.EFCore/CloudNimble.EasyAF.Data.EFCore.csproj +++ b/src/CloudNimble.EasyAF.Data.EFCore/CloudNimble.EasyAF.Data.EFCore.csproj @@ -1,27 +1,31 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; $(DocumentationFile)\$(AssemblyName).xml EFCORE - + + + + + - + - + - + diff --git a/src/CloudNimble.EasyAF.Docs/assembly-list.txt b/src/CloudNimble.EasyAF.Docs/assembly-list.txt index 44643af..e432976 100644 --- a/src/CloudNimble.EasyAF.Docs/assembly-list.txt +++ b/src/CloudNimble.EasyAF.Docs/assembly-list.txt @@ -1,19 +1,19 @@ -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Business\bin\Debug\net10.0\CloudNimble.EasyAF.Business.EF6.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Business.EFCore\bin\Debug\net10.0\CloudNimble.EasyAF.Business.EFCore.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Configuration\bin\Debug\net10.0\CloudNimble.EasyAF.Configuration.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Core\bin\Debug\net10.0\CloudNimble.EasyAF.Core.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Business\bin\Debug\net11.0\CloudNimble.EasyAF.Business.EF6.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Business.EFCore\bin\Debug\net11.0\CloudNimble.EasyAF.Business.EFCore.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Configuration\bin\Debug\net11.0\CloudNimble.EasyAF.Configuration.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Core\bin\Debug\net11.0\CloudNimble.EasyAF.Core.dll D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Data.EF6\bin\Debug\net48\CloudNimble.EasyAF.Data.EF6.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Data.EFCore\bin\Debug\net10.0\CloudNimble.EasyAF.Data.EFCore.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Data.EFCore\bin\Debug\net11.0\CloudNimble.EasyAF.Data.EFCore.dll D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Extensions.Collections\bin\Debug\net10.0\CloudNimble.EasyAF.Extensions.Collections.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Http\bin\Debug\net10.0\CloudNimble.EasyAF.Http.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Http.NewtonsoftJson\bin\Debug\net10.0\CloudNimble.EasyAF.Http.NewtonsoftJson.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Http.SystemTextJson\bin\Debug\net10.0\CloudNimble.EasyAF.Http.SystemTextJson.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.MSBuild\bin\Debug\net10.0\CloudNimble.EasyAF.MSBuild.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.NewtonsoftJson.Compatibility\bin\Debug\net10.0\CloudNimble.EasyAF.NewtonsoftJson.Compatibility.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.ODataClient\bin\Debug\net10.0\CloudNimble.EasyAF.ODataClient.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Restier\bin\Debug\net10.0\CloudNimble.EasyAF.Restier.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Restier.Breakdance\bin\Debug\net10.0\CloudNimble.EasyAF.Restier.Breakdance.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Restier.EF6\bin\Debug\net10.0\CloudNimble.EasyAF.Restier.EF6.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Restier.EFCore\bin\Debug\net10.0\CloudNimble.EasyAF.Restier.EFCore.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Tools\bin\Debug\net10.0\CloudNimble.EasyAF.Tools.dll -D:\GitHub\EasyAF\src\CloudNimble.EasyAF.XmlDocumentation\bin\Debug\net10.0\CloudNimble.EasyAF.XmlDocumentation.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Http\bin\Debug\net11.0\CloudNimble.EasyAF.Http.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Http.NewtonsoftJson\bin\Debug\net11.0\CloudNimble.EasyAF.Http.NewtonsoftJson.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Http.SystemTextJson\bin\Debug\net11.0\CloudNimble.EasyAF.Http.SystemTextJson.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.MSBuild\bin\Debug\net11.0\CloudNimble.EasyAF.MSBuild.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.NewtonsoftJson.Compatibility\bin\Debug\net11.0\CloudNimble.EasyAF.NewtonsoftJson.Compatibility.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.ODataClient\bin\Debug\net11.0\CloudNimble.EasyAF.ODataClient.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Restier\bin\Debug\net11.0\CloudNimble.EasyAF.Restier.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Restier.Breakdance\bin\Debug\net11.0\CloudNimble.EasyAF.Restier.Breakdance.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Restier.EF6\bin\Debug\net11.0\CloudNimble.EasyAF.Restier.EF6.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Restier.EFCore\bin\Debug\net11.0\CloudNimble.EasyAF.Restier.EFCore.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.Tools\bin\Debug\net11.0\CloudNimble.EasyAF.Tools.dll +D:\GitHub\EasyAF\src\CloudNimble.EasyAF.XmlDocumentation\bin\Debug\net11.0\CloudNimble.EasyAF.XmlDocumentation.dll diff --git a/src/CloudNimble.EasyAF.EFCoreToEdmx/CloudNimble.EasyAF.EFCoreToEdmx.csproj b/src/CloudNimble.EasyAF.EFCoreToEdmx/CloudNimble.EasyAF.EFCoreToEdmx.csproj index 182f932..d146b45 100644 --- a/src/CloudNimble.EasyAF.EFCoreToEdmx/CloudNimble.EasyAF.EFCoreToEdmx.csproj +++ b/src/CloudNimble.EasyAF.EFCoreToEdmx/CloudNimble.EasyAF.EFCoreToEdmx.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; $(DocumentationFile)\$(AssemblyName).xml $(NoWarn);CA1822;EF1001;NU1701; true @@ -12,6 +12,18 @@ + + + + + + + + + + + + diff --git a/src/CloudNimble.EasyAF.EFCoreToEdmx/EasyAFLongDescription.cs b/src/CloudNimble.EasyAF.EFCoreToEdmx/EasyAFLongDescription.cs new file mode 100644 index 0000000..7975896 --- /dev/null +++ b/src/CloudNimble.EasyAF.EFCoreToEdmx/EasyAFLongDescription.cs @@ -0,0 +1,229 @@ +using CloudNimble.EasyAF.EFCoreToEdmx.Models; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Xml.Linq; + +namespace CloudNimble.EasyAF.EFCoreToEdmx +{ + + /// + /// Copies the SQL Server EasyAF_LongDescription extended property into CSDL LongDescription, + /// and preserves hand-authored CSDL docs when the catalog property is missing. + /// + internal static class EasyAFLongDescription + { + + public const string PropertyName = "EasyAF_LongDescription"; + + private const string Query = """ + SELECT + OBJECT_SCHEMA_NAME(ep.major_id) AS [Schema], + OBJECT_NAME(ep.major_id) AS [Table], + COL_NAME(ep.major_id, ep.minor_id) AS [Column], + CONVERT(nvarchar(max), ep.value) AS [Value] + FROM sys.extended_properties AS ep + WHERE ep.class = 1 + AND ep.name = N'EasyAF_LongDescription' + """; + + /// + /// One round-trip: every EasyAF_LongDescription on tables and columns. + /// Table-level rows use a null column key. + /// + public static Dictionary<(string Table, string Column), string> Load(DbConnection connection) + { + ArgumentNullException.ThrowIfNull(connection); + + var shouldClose = connection.State != ConnectionState.Open; + if (shouldClose) + { + connection.Open(); + } + + try + { + using var command = connection.CreateCommand(); + command.CommandText = Query; + using var reader = command.ExecuteReader(); + var catalog = new Dictionary<(string Table, string Column), string>(); + while (reader.Read()) + { + var table = reader.IsDBNull(1) ? null : reader.GetString(1); + var column = reader.IsDBNull(2) ? null : reader.GetString(2); + var value = reader.IsDBNull(3) ? null : reader.GetString(3); + if (string.IsNullOrEmpty(table) || string.IsNullOrEmpty(value)) + { + continue; + } + + catalog[Key(table, column)] = value; + } + + return catalog; + } + finally + { + if (shouldClose && connection.State == ConnectionState.Open) + { + connection.Close(); + } + } + } + + /// + /// Stamps catalog values onto the model by looking each entity/property up in memory. + /// + public static void Apply(EdmxModel model, IReadOnlyDictionary<(string Table, string Column), string> catalog) + { + ArgumentNullException.ThrowIfNull(model); + if (catalog is null || catalog.Count == 0) + { + return; + } + + catalog = catalog.ToDictionary(kv => Key(kv.Key.Table, kv.Key.Column), kv => kv.Value); + var entitiesByName = model.EntityTypes.ToDictionary(e => e.Name, StringComparer.OrdinalIgnoreCase); + + foreach (var entitySet in model.EntitySets) + { + if (!entitiesByName.TryGetValue(entitySet.EntityTypeName, out var entity)) + { + continue; + } + + var table = entitySet.Name; + if (catalog.TryGetValue(Key(table, null), out var tableDoc)) + { + entity.LongDescription = tableDoc; + } + + foreach (var property in entity.Properties) + { + var column = string.IsNullOrEmpty(property.StoreColumnName) ? property.Name : property.StoreColumnName; + if (catalog.TryGetValue(Key(table, column), out var columnDoc)) + { + property.LongDescription = columnDoc; + } + } + } + } + + /// + /// Builds an in-memory catalog from rows (tests / callers that already have the set) and applies it. + /// + public static void Apply(EdmxModel model, IEnumerable<(string Table, string Column, string Value)> rows) + { + ArgumentNullException.ThrowIfNull(model); + if (rows is null) + { + return; + } + + var catalog = new Dictionary<(string Table, string Column), string>(); + foreach (var (table, column, value) in rows) + { + if (string.IsNullOrEmpty(table) || string.IsNullOrEmpty(value)) + { + continue; + } + + catalog[Key(table, column)] = value; + } + + Apply(model, catalog); + } + + /// + /// Loads the full catalog in one query, then stamps the model from memory. + /// Best-effort: callers should catch failures so generate still succeeds. + /// + public static void ApplyFromConnection(EdmxModel model, DbConnection connection) + { + Apply(model, Load(connection)); + } + + /// + /// Fills empty Summary / LongDescription from an existing EDMX. Does not overwrite catalog values already set. + /// + public static void PreserveExisting(EdmxModel model, string existingEdmx) + { + ArgumentNullException.ThrowIfNull(model); + if (string.IsNullOrWhiteSpace(existingEdmx)) + { + return; + } + + var doc = XDocument.Parse(existingEdmx); + var conceptual = doc.Descendants().FirstOrDefault(x => x.Name.LocalName == "ConceptualModels"); + if (conceptual is null) + { + return; + } + + foreach (var entityElement in conceptual.Descendants().Where(x => x.Name.LocalName == "EntityType")) + { + var name = entityElement.Attribute("Name")?.Value; + var entity = model.EntityTypes.FirstOrDefault(e => e.Name == name); + if (entity is null) + { + continue; + } + + var (summary, longDescription) = ReadDocs(entityElement); + if (string.IsNullOrEmpty(entity.Documentation) && !string.IsNullOrEmpty(summary)) + { + entity.Documentation = summary; + } + + if (string.IsNullOrEmpty(entity.LongDescription) && !string.IsNullOrEmpty(longDescription)) + { + entity.LongDescription = longDescription; + } + + foreach (var propertyElement in entityElement.Elements().Where(x => x.Name.LocalName == "Property")) + { + var propertyName = propertyElement.Attribute("Name")?.Value; + var property = entity.Properties.FirstOrDefault(p => p.Name == propertyName); + if (property is null) + { + continue; + } + + var (propSummary, propLong) = ReadDocs(propertyElement); + if (string.IsNullOrEmpty(property.Documentation) && !string.IsNullOrEmpty(propSummary)) + { + property.Documentation = propSummary; + } + + if (string.IsNullOrEmpty(property.LongDescription) && !string.IsNullOrEmpty(propLong)) + { + property.LongDescription = propLong; + } + } + } + } + + private static (string Summary, string LongDescription) ReadDocs(XElement owner) + { + var documentation = owner.Elements().FirstOrDefault(x => x.Name.LocalName == "Documentation"); + if (documentation is null) + { + return (null, null); + } + + var summary = documentation.Elements().FirstOrDefault(x => x.Name.LocalName == "Summary")?.Value; + var longDescription = documentation.Elements().FirstOrDefault(x => x.Name.LocalName == "LongDescription")?.Value; + return (summary, longDescription); + } + + private static (string Table, string Column) Key(string table, string column) + { + return (table.ToLowerInvariant(), column?.ToLowerInvariant()); + } + + } + +} diff --git a/src/CloudNimble.EasyAF.EFCoreToEdmx/EdmxConverter.cs b/src/CloudNimble.EasyAF.EFCoreToEdmx/EdmxConverter.cs index 61e6b99..a10f338 100644 --- a/src/CloudNimble.EasyAF.EFCoreToEdmx/EdmxConverter.cs +++ b/src/CloudNimble.EasyAF.EFCoreToEdmx/EdmxConverter.cs @@ -239,6 +239,7 @@ public async Task ConvertToEdmxFileAsync(DbContext context, string filePath) /// /// The path to the .edmx.config file containing scaffolding settings. /// The path to the project directory containing configuration files. + /// Optional existing EDMX XML whose empty Summary / LongDescription fields are preserved when the catalog has no value. /// A tuple containing the EDMX XML content and the extracted OnModelCreating method body. /// Thrown when parameters are null or empty. /// Thrown when the configuration file is not found. @@ -255,7 +256,7 @@ public async Task ConvertToEdmxFileAsync(DbContext context, string filePath) /// var (edmxContent, onModelCreating) = await converter.ConvertFromDatabaseAsync("MyModel.edmx.config", @"C:\MyProject"); /// /// - public async Task<(string EdmxContent, string OnModelCreatingBody)> ConvertFromDatabaseAsync(string configPath, string projectPath) + public async Task<(string EdmxContent, string OnModelCreatingBody)> ConvertFromDatabaseAsync(string configPath, string projectPath, string existingEdmx = null) { ArgumentException.ThrowIfNullOrWhiteSpace(configPath, nameof(configPath)); ArgumentException.ThrowIfNullOrWhiteSpace(projectPath, nameof(projectPath)); @@ -292,6 +293,20 @@ public async Task ConvertToEdmxFileAsync(DbContext context, string filePath) // Store the OnModelCreating body in the model edmxModel.OnModelCreatingBody = scaffoldingResult.OnModelCreatingBody; + if (providerType == DatabaseProviderType.SqlServer) + { + try + { + EasyAFLongDescription.ApplyFromConnection(edmxModel, scaffoldingResult.Context.Database.GetDbConnection()); + } + catch (Exception) + { + // Catalog lookup is advisory; generate still succeeds without it. + } + } + + EasyAFLongDescription.PreserveExisting(edmxModel, existingEdmx); + // Generate the EDMX XML var xmlGenerator = new EdmxXmlGenerator(edmxModel, providerType, tableInfos); var edmxContent = xmlGenerator.Generate(); @@ -330,7 +345,8 @@ public async Task ConvertToEdmxFileAsync(DbContext context, string filePath) ArgumentException.ThrowIfNullOrWhiteSpace(projectPath, nameof(projectPath)); var configPath = edmxPath + ".config"; - return await ConvertFromDatabaseAsync(configPath, projectPath); + var existingEdmx = File.Exists(edmxPath) ? await File.ReadAllTextAsync(edmxPath) : null; + return await ConvertFromDatabaseAsync(configPath, projectPath, existingEdmx); } /// diff --git a/src/CloudNimble.EasyAF.EFCoreToEdmx/EdmxXmlGenerator.cs b/src/CloudNimble.EasyAF.EFCoreToEdmx/EdmxXmlGenerator.cs index 28e125e..e57fc42 100644 --- a/src/CloudNimble.EasyAF.EFCoreToEdmx/EdmxXmlGenerator.cs +++ b/src/CloudNimble.EasyAF.EFCoreToEdmx/EdmxXmlGenerator.cs @@ -144,6 +144,33 @@ public string Generate() #region Private Methods + /// + /// Builds a CSDL/SSDL Documentation element. LongDescription is CSDL-only. + /// Summary-only does not emit an empty LongDescription. + /// + internal static XElement CreateDocumentationElement(XNamespace ns, string summary, string longDescription, bool includeLongDescription) + { + var hasSummary = !string.IsNullOrWhiteSpace(summary); + var hasLong = includeLongDescription && !string.IsNullOrWhiteSpace(longDescription); + if (!hasSummary && !hasLong) + { + return null; + } + + var element = new XElement(ns + "Documentation"); + if (hasSummary) + { + element.Add(new XElement(ns + "Summary", summary)); + } + + if (hasLong) + { + element.Add(new XElement(ns + "LongDescription", longDescription)); + } + + return element; + } + /// /// Creates EF Core's design-time pluralization service. /// @@ -227,12 +254,10 @@ private XElement GenerateConceptualEntityType(EdmxEntityType entityType) new XAttribute("Name", entityType.Name) // Keep as singular ); - // Add documentation if available - if (!string.IsNullOrWhiteSpace(entityType.Documentation)) + var documentation = CreateDocumentationElement(_edmNs, entityType.Documentation, entityType.LongDescription, includeLongDescription: true); + if (documentation is not null) { - element.Add(new XElement(_edmNs + "Documentation", - new XElement(_edmNs + "Summary", entityType.Documentation) - )); + element.Add(documentation); } // Add keys @@ -320,12 +345,10 @@ private XElement GenerateConceptualProperty(EdmxProperty property) element.Add(new XAttribute("DefaultValue", property.DefaultValue)); } - // Add documentation if available - if (!string.IsNullOrWhiteSpace(property.Documentation)) + var documentation = CreateDocumentationElement(_edmNs, property.Documentation, property.LongDescription, includeLongDescription: true); + if (documentation is not null) { - element.Add(new XElement(_edmNs + "Documentation", - new XElement(_edmNs + "Summary", property.Documentation) - )); + element.Add(documentation); } return element; @@ -523,12 +546,10 @@ private XElement GenerateStorageEntityType(EdmxEntityType entityType) new XAttribute("Name", pluralEntityTypeName) ); - // Add documentation if available - if (!string.IsNullOrWhiteSpace(entityType.Documentation)) + var documentation = CreateDocumentationElement(_ssdlNs, entityType.Documentation, entityType.LongDescription, includeLongDescription: false); + if (documentation is not null) { - element.Add(new XElement(_ssdlNs + "Documentation", - new XElement(_ssdlNs + "Summary", entityType.Documentation) - )); + element.Add(documentation); } // Add key - need to map key names to their store column names diff --git a/src/CloudNimble.EasyAF.EFCoreToEdmx/Models/EdmxEntityType.cs b/src/CloudNimble.EasyAF.EFCoreToEdmx/Models/EdmxEntityType.cs index 5990f1b..91102de 100644 --- a/src/CloudNimble.EasyAF.EFCoreToEdmx/Models/EdmxEntityType.cs +++ b/src/CloudNimble.EasyAF.EFCoreToEdmx/Models/EdmxEntityType.cs @@ -35,10 +35,15 @@ public class EdmxEntityType public List Keys { get; set; } = new(); /// - /// Gets or sets the documentation comment for the entity type. + /// Gets or sets the CSDL Summary (from MS_Description / HasComment). /// public string Documentation { get; set; } = string.Empty; + /// + /// Gets or sets the CSDL LongDescription (from the EasyAF_LongDescription extended property). + /// + public string LongDescription { get; set; } = string.Empty; + } } diff --git a/src/CloudNimble.EasyAF.EFCoreToEdmx/Models/EdmxProperty.cs b/src/CloudNimble.EasyAF.EFCoreToEdmx/Models/EdmxProperty.cs index 1817b5a..05a1ed1 100644 --- a/src/CloudNimble.EasyAF.EFCoreToEdmx/Models/EdmxProperty.cs +++ b/src/CloudNimble.EasyAF.EFCoreToEdmx/Models/EdmxProperty.cs @@ -52,10 +52,15 @@ public class EdmxProperty public bool? IsUnicode { get; set; } /// - /// Gets or sets the documentation comment for the property. + /// Gets or sets the CSDL Summary (from MS_Description / HasComment). /// public string Documentation { get; set; } = string.Empty; + /// + /// Gets or sets the CSDL LongDescription (from the EasyAF_LongDescription extended property). + /// + public string LongDescription { get; set; } = string.Empty; + /// /// Gets or sets the default value for the property. /// diff --git a/src/CloudNimble.EasyAF.Http.NewtonsoftJson/CloudNimble.EasyAF.Http.NewtonsoftJson.csproj b/src/CloudNimble.EasyAF.Http.NewtonsoftJson/CloudNimble.EasyAF.Http.NewtonsoftJson.csproj index e62a7d7..d725230 100644 --- a/src/CloudNimble.EasyAF.Http.NewtonsoftJson/CloudNimble.EasyAF.Http.NewtonsoftJson.csproj +++ b/src/CloudNimble.EasyAF.Http.NewtonsoftJson/CloudNimble.EasyAF.Http.NewtonsoftJson.csproj @@ -1,12 +1,12 @@  - net10.0;net9.0;net8.0;netstandard2.0; + net11.0;net10.0;net9.0;net8.0;netstandard2.0; $(DocumentationFile)\$(AssemblyName).xml - + diff --git a/src/CloudNimble.EasyAF.Http.SystemTextJson/CloudNimble.EasyAF.Http.SystemTextJson.csproj b/src/CloudNimble.EasyAF.Http.SystemTextJson/CloudNimble.EasyAF.Http.SystemTextJson.csproj index a64d603..ec340ee 100644 --- a/src/CloudNimble.EasyAF.Http.SystemTextJson/CloudNimble.EasyAF.Http.SystemTextJson.csproj +++ b/src/CloudNimble.EasyAF.Http.SystemTextJson/CloudNimble.EasyAF.Http.SystemTextJson.csproj @@ -1,7 +1,7 @@ - net10.0;net9.0;net8.0;netstandard2.0; + net11.0;net10.0;net9.0;net8.0;netstandard2.0; $(DocumentationFile)\$(AssemblyName).xml diff --git a/src/CloudNimble.EasyAF.Http/CloudNimble.EasyAF.Http.csproj b/src/CloudNimble.EasyAF.Http/CloudNimble.EasyAF.Http.csproj index 6d57a2f..dfc2e72 100644 --- a/src/CloudNimble.EasyAF.Http/CloudNimble.EasyAF.Http.csproj +++ b/src/CloudNimble.EasyAF.Http/CloudNimble.EasyAF.Http.csproj @@ -1,36 +1,43 @@  - net10.0;net9.0;net8.0;netstandard2.0 + net11.0;net10.0;net9.0;net8.0;netstandard2.0 $(DocumentationFile)\$(AssemblyName).xml + + + + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + diff --git a/src/CloudNimble.EasyAF.MSBuild/CloudNimble.EasyAF.MSBuild.csproj b/src/CloudNimble.EasyAF.MSBuild/CloudNimble.EasyAF.MSBuild.csproj index 837aa0c..c09f70d 100644 --- a/src/CloudNimble.EasyAF.MSBuild/CloudNimble.EasyAF.MSBuild.csproj +++ b/src/CloudNimble.EasyAF.MSBuild/CloudNimble.EasyAF.MSBuild.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0;net472 + net11.0;net10.0;net9.0;net8.0;net472 Provides MSBuild project management capabilities for the EasyAF framework, including project file manipulation and property management. CloudNimble;EasyAF;EasyApplicationFramework;MSBuild;ProjectManagement $(DocumentationFile)\$(AssemblyName).xml @@ -18,18 +18,25 @@ - - - - + + + + - + + + + + + + + @@ -40,4 +47,4 @@ - \ No newline at end of file + diff --git a/src/CloudNimble.EasyAF.NewtonsoftJson.Compatibility/CloudNimble.EasyAF.NewtonsoftJson.Compatibility.csproj b/src/CloudNimble.EasyAF.NewtonsoftJson.Compatibility/CloudNimble.EasyAF.NewtonsoftJson.Compatibility.csproj index 1536f6d..7b8e731 100644 --- a/src/CloudNimble.EasyAF.NewtonsoftJson.Compatibility/CloudNimble.EasyAF.NewtonsoftJson.Compatibility.csproj +++ b/src/CloudNimble.EasyAF.NewtonsoftJson.Compatibility/CloudNimble.EasyAF.NewtonsoftJson.Compatibility.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0;netstandard2.0; + net11.0;net10.0;net9.0;net8.0;netstandard2.0; $(DocumentationFile)\$(AssemblyName).xml @@ -24,7 +24,7 @@ - + diff --git a/src/CloudNimble.EasyAF.ODataClient/CloudNimble.EasyAF.ODataClient.csproj b/src/CloudNimble.EasyAF.ODataClient/CloudNimble.EasyAF.ODataClient.csproj index ff2667d..1290705 100644 --- a/src/CloudNimble.EasyAF.ODataClient/CloudNimble.EasyAF.ODataClient.csproj +++ b/src/CloudNimble.EasyAF.ODataClient/CloudNimble.EasyAF.ODataClient.csproj @@ -3,24 +3,29 @@ CloudNimble.EasyAF.OData CloudNimble.EasyAF.ODataClient - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; $(DocumentationFile)\$(AssemblyName).xml + + + + + - + - + - + diff --git a/src/CloudNimble.EasyAF.Restier.Breakdance/CloudNimble.EasyAF.Restier.Breakdance.csproj b/src/CloudNimble.EasyAF.Restier.Breakdance/CloudNimble.EasyAF.Restier.Breakdance.csproj index 3f58b1e..0111a2a 100644 --- a/src/CloudNimble.EasyAF.Restier.Breakdance/CloudNimble.EasyAF.Restier.Breakdance.csproj +++ b/src/CloudNimble.EasyAF.Restier.Breakdance/CloudNimble.EasyAF.Restier.Breakdance.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; $(DocumentationFile)\$(AssemblyName).xml @@ -12,16 +12,20 @@ + + + + - + - + - + diff --git a/src/CloudNimble.EasyAF.Restier.EF6/CloudNimble.EasyAF.Restier.EF6.csproj b/src/CloudNimble.EasyAF.Restier.EF6/CloudNimble.EasyAF.Restier.EF6.csproj index 9f94023..0275f84 100644 --- a/src/CloudNimble.EasyAF.Restier.EF6/CloudNimble.EasyAF.Restier.EF6.csproj +++ b/src/CloudNimble.EasyAF.Restier.EF6/CloudNimble.EasyAF.Restier.EF6.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; $(DocumentationFile)\$(AssemblyName).xml EF6 @@ -16,19 +16,24 @@ + + + + + - - + + - - + + - - + + diff --git a/src/CloudNimble.EasyAF.Restier.EFCore/CloudNimble.EasyAF.Restier.EFCore.csproj b/src/CloudNimble.EasyAF.Restier.EFCore/CloudNimble.EasyAF.Restier.EFCore.csproj index 5a2ac63..7717763 100644 --- a/src/CloudNimble.EasyAF.Restier.EFCore/CloudNimble.EasyAF.Restier.EFCore.csproj +++ b/src/CloudNimble.EasyAF.Restier.EFCore/CloudNimble.EasyAF.Restier.EFCore.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; $(DocumentationFile)\$(AssemblyName).xml EFCORE @@ -16,19 +16,24 @@ + + + + + - - + + - - + + - - + + diff --git a/src/CloudNimble.EasyAF.Restier/CloudNimble.EasyAF.Restier.csproj b/src/CloudNimble.EasyAF.Restier/CloudNimble.EasyAF.Restier.csproj index a51c659..7c369d4 100644 --- a/src/CloudNimble.EasyAF.Restier/CloudNimble.EasyAF.Restier.csproj +++ b/src/CloudNimble.EasyAF.Restier/CloudNimble.EasyAF.Restier.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; $(DocumentationFile)\$(AssemblyName).xml diff --git a/src/CloudNimble.EasyAF.Tests.Business/CloudNimble.EasyAF.Tests.Business.EF6.csproj b/src/CloudNimble.EasyAF.Tests.Business/CloudNimble.EasyAF.Tests.Business.EF6.csproj index 52c7eda..659b5bc 100644 --- a/src/CloudNimble.EasyAF.Tests.Business/CloudNimble.EasyAF.Tests.Business.EF6.csproj +++ b/src/CloudNimble.EasyAF.Tests.Business/CloudNimble.EasyAF.Tests.Business.EF6.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; false $(NoWarn);CA1822; @@ -11,17 +11,7 @@ - - - - - - - - - - diff --git a/src/CloudNimble.EasyAF.Tests.Business/EntityManagerTests.cs b/src/CloudNimble.EasyAF.Tests.Business/EntityManagerTests.cs index 51b4061..702733e 100644 --- a/src/CloudNimble.EasyAF.Tests.Business/EntityManagerTests.cs +++ b/src/CloudNimble.EasyAF.Tests.Business/EntityManagerTests.cs @@ -4,7 +4,6 @@ using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; -using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -14,6 +13,7 @@ namespace CloudNimble.EasyAF.Tests.Business { [TestClass] [TestCategory("RequiresDatabase")] + [DoNotParallelize] public class EntityManagerTests : EasyAFBusinessTestBase { @@ -29,6 +29,12 @@ public void TestInitialize() TestSetup(); } + [TestCleanup] + public void TestCleanup() + { + TestTearDown(); + } + #endregion [TestMethod] @@ -90,49 +96,64 @@ public async Task ProductManager_CreatedByIdIsPopulated() [TestMethod] public async Task ProductManager_CanDeleteByExpression() { + var dbContext = GetScopedService(); var keepStatusTypeId = Guid.NewGuid(); var deleteStatusTypeId = Guid.NewGuid(); + var keepProductId = Guid.NewGuid(); + var deleteProductId1 = Guid.NewGuid(); + var deleteProductId2 = Guid.NewGuid(); + var productIds = new[] { keepProductId, deleteProductId1, deleteProductId2 }; - var dbContext = GetScopedService(); - - var statuses = dbContext.ProductStatusTypes.Where(c => c.SortOrder < 2).ToList(); - if (!statuses.Any(c => c.SortOrder == 0)) + try { - dbContext.ProductStatusTypes.Add(new ProductStatusType { Id = keepStatusTypeId, DisplayName = "Started", SortOrder = 0 }); + dbContext.ProductStatusTypes.AddRange( + [ + new ProductStatusType + { + Id = keepStatusTypeId, + DisplayName = $"Keep_{keepStatusTypeId:N}", + SortOrder = int.MaxValue - 1, + IsActive = true, + DateCreated = DateTimeOffset.UtcNow + }, + new ProductStatusType + { + Id = deleteStatusTypeId, + DisplayName = $"Delete_{deleteStatusTypeId:N}", + SortOrder = int.MaxValue, + IsActive = true, + DateCreated = DateTimeOffset.UtcNow + } + ]); await dbContext.SaveChangesAsync(); - } - else - { - keepStatusTypeId = statuses.First(c => c.SortOrder == 0).Id; - } - if (!statuses.Any(c => c.SortOrder == 1)) - { - dbContext.ProductStatusTypes.Add(new ProductStatusType { Id = deleteStatusTypeId, DisplayName = "Reviewed", SortOrder = 1 }); + dbContext.Products.AddRange( + [ + new() { Id = keepProductId, DisplayName = $"Keep_{keepProductId:N}", StatusTypeId = keepStatusTypeId }, + new() { Id = deleteProductId1, DisplayName = $"Delete_{deleteProductId1:N}", StatusTypeId = deleteStatusTypeId }, + new() { Id = deleteProductId2, DisplayName = $"Delete_{deleteProductId2:N}", StatusTypeId = deleteStatusTypeId } + ]); await dbContext.SaveChangesAsync(); - } - else - { - deleteStatusTypeId = statuses.First(c => c.SortOrder == 1).Id; - } - - dbContext.Products.AddRange( - [ - new() { Id = Guid.NewGuid(), DisplayName= "DeleteUnitTest", StatusTypeId = keepStatusTypeId }, - new() { Id = Guid.NewGuid(), DisplayName= "DeleteUnitTest", StatusTypeId = deleteStatusTypeId }, - new() { Id = Guid.NewGuid(), DisplayName= "DeleteUnitTest", StatusTypeId = deleteStatusTypeId } - ]); - await dbContext.SaveChangesAsync(); + var manager = new ProductManager(dbContext, null); + var count = await manager.DeleteByStatusType(deleteStatusTypeId); - var manager = new ProductManager(dbContext, null); - var count = await manager.DeleteByStatusType(deleteStatusTypeId); + count.Should().Be(2); - count.Should().Be(2); - dbContext.Products.Should().NotContain(c => c.StatusTypeId == deleteStatusTypeId); - dbContext.Products.Should().HaveCount(1); + var remainingIds = dbContext.Products.AsNoTracking() + .Where(p => productIds.Contains(p.Id)) + .Select(p => p.Id) + .ToList(); - await dbContext.Products.DeleteAsync(); + remainingIds.Should().ContainSingle().Which.Should().Be(keepProductId); + } + finally + { + await dbContext.Products.Where(p => productIds.Contains(p.Id)).DeleteAsync(); + await dbContext.ProductStatusTypes + .Where(s => s.Id == keepStatusTypeId || s.Id == deleteStatusTypeId) + .DeleteAsync(); + } } @@ -144,28 +165,37 @@ public async Task ProductManager_CanDeleteByExpression() public async Task ProductManager_ResetAuditProperties_CanResetProduct() { var dbContext = GetScopedService(); - var product = new Product { DisplayName = "Test", }; - var manager = new ProductManager(dbContext, null); - await manager.InsertAsync(product, false); - product.CreatedById.Should().NotBeEmpty(); - product.DateCreated.Should().BeCloseTo(DateTimeOffset.Now, new TimeSpan(0, 0, 1)); + try + { + await manager.InsertAsync(product, false); + + product.CreatedById.Should().NotBeEmpty(); + product.DateCreated.Should().BeCloseTo(DateTimeOffset.Now, new TimeSpan(0, 0, 1)); - await manager.UpdateAsync(product, false); - product.UpdatedById.Should().NotBeEmpty(); - product.DateUpdated.Should().BeCloseTo(DateTimeOffset.Now, new TimeSpan(0, 0, 1)); + await manager.UpdateAsync(product, false); + product.UpdatedById.Should().NotBeEmpty(); + product.DateUpdated.Should().BeCloseTo(DateTimeOffset.Now, new TimeSpan(0, 0, 1)); - manager.ResetAuditProperties(product); + manager.ResetAuditProperties(product); - product.CreatedById.Should().NotBeEmpty(); - product.DateCreated.Should().BeCloseTo(DateTimeOffset.Now, new TimeSpan(0, 0, 1)); - product.UpdatedById.Should().BeNull(); - product.DateUpdated.Should().BeNull(); + product.CreatedById.Should().NotBeEmpty(); + product.DateCreated.Should().BeCloseTo(DateTimeOffset.Now, new TimeSpan(0, 0, 1)); + product.UpdatedById.Should().BeNull(); + product.DateUpdated.Should().BeNull(); + } + finally + { + if (product.Id != Guid.Empty) + { + await dbContext.Products.Where(p => p.Id == product.Id).DeleteAsync(); + } + } } /// @@ -176,30 +206,38 @@ public async Task ProductManager_ResetAuditProperties_CanResetProduct() public async Task ProductManager_ResetAuditProperties_CanResetInquiry() { var dbContext = GetScopedService(); - var inquiry = new Inquiry { Message = "Test", }; - var manager = new ProductManager(dbContext, null); var inquiryManager = new InquiryManager(dbContext, null); - await inquiryManager.InsertAsync(inquiry, false); + try + { + await inquiryManager.InsertAsync(inquiry, false); - inquiry.CreatedById.Should().NotBeEmpty(); - inquiry.DateCreated.Should().BeCloseTo(DateTimeOffset.Now, new TimeSpan(0, 0, 1)); + inquiry.CreatedById.Should().NotBeEmpty(); + inquiry.DateCreated.Should().BeCloseTo(DateTimeOffset.Now, new TimeSpan(0, 0, 1)); - await inquiryManager.UpdateAsync(inquiry, false); - inquiry.UpdatedById.Should().NotBeEmpty(); - inquiry.DateUpdated.Should().BeCloseTo(DateTimeOffset.Now, new TimeSpan(0, 0, 1)); + await inquiryManager.UpdateAsync(inquiry, false); + inquiry.UpdatedById.Should().NotBeEmpty(); + inquiry.DateUpdated.Should().BeCloseTo(DateTimeOffset.Now, new TimeSpan(0, 0, 1)); - manager.ResetAuditProperties(inquiry); + manager.ResetAuditProperties(inquiry); - inquiry.CreatedById.Should().NotBeEmpty(); - inquiry.DateCreated.Should().BeCloseTo(DateTimeOffset.Now, new TimeSpan(0, 0, 1)); - inquiry.UpdatedById.Should().BeNull(); - inquiry.DateUpdated.Should().BeNull(); + inquiry.CreatedById.Should().NotBeEmpty(); + inquiry.DateCreated.Should().BeCloseTo(DateTimeOffset.Now, new TimeSpan(0, 0, 1)); + inquiry.UpdatedById.Should().BeNull(); + inquiry.DateUpdated.Should().BeNull(); + } + finally + { + if (inquiry.Id != Guid.Empty) + { + await dbContext.Inquiries.Where(i => i.Id == inquiry.Id).DeleteAsync(); + } + } } //RWM: Reset object tests here: diff --git a/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/DbContexts/EasyAFEntities.Generated.cs b/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/DbContexts/EasyAFEntities.Generated.cs index 7ec90e2..972e689 100644 --- a/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/DbContexts/EasyAFEntities.Generated.cs +++ b/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/DbContexts/EasyAFEntities.Generated.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by EasyAF's Code Generators. -// Date Generated: 11/20/2024 11:29:50 PM +// Date Generated: 9/14/2026 3:48:34 AM // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -16,37 +16,19 @@ namespace EasyAFModel { - /// - /// - /// public partial class EasyAFEntities : DbContext { #region Public Properties - /// - /// - /// public virtual DbSet Inquiries { get; set; } - /// - /// - /// public virtual DbSet InquiryStateTypes { get; set; } - /// - /// - /// public virtual DbSet Products { get; set; } - /// - /// - /// public virtual DbSet ProductStatusTypes { get; set; } - /// - /// - /// public virtual DbSet Users { get; set; } #endregion diff --git a/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/Entities/Inquiry.Generated.cs b/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/Entities/Inquiry.Generated.cs index d4b844c..e8fb587 100644 --- a/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/Entities/Inquiry.Generated.cs +++ b/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/Entities/Inquiry.Generated.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by EasyAF's Code Generators. -// Date Generated: 11/28/2025 7:08:07 PM +// Date Generated: 9/14/2026 3:48:07 AM // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -18,9 +18,6 @@ namespace EasyAFModel { - /// - /// - /// public partial class Inquiry : DbObservableObject, IHasState, ICreatedAuditable, ICreatorTrackable, IUpdatedAuditable, IUpdaterTrackable { @@ -41,18 +38,12 @@ public partial class Inquiry : DbObservableObject, IHasState, #region Public Properties - /// - /// - /// public Guid Id { get => _id; set => Set(() => Id, ref _id, value); } - /// - /// - /// [StringLength(200)] public string Subject { @@ -60,9 +51,6 @@ public string Subject set => Set(() => Subject, ref _subject, value); } - /// - /// - /// [StringLength(1000)] public string Message { @@ -70,63 +58,42 @@ public string Message set => Set(() => Message, ref _message, value); } - /// - /// - /// public Guid StateTypeId { get => _stateTypeId; set => Set(() => StateTypeId, ref _stateTypeId, value); } - /// - /// - /// public Guid CreatedById { get => _createdById; set => Set(() => CreatedById, ref _createdById, value); } - /// - /// - /// public DateTimeOffset DateCreated { get => _dateCreated; set => Set(() => DateCreated, ref _dateCreated, value); } - /// - /// - /// public Nullable UpdatedById { get => _updatedById; set => Set(() => UpdatedById, ref _updatedById, value); } - /// - /// - /// public Nullable DateUpdated { get => _dateUpdated; set => Set(() => DateUpdated, ref _dateUpdated, value); } - /// - /// - /// public InquiryStateType StateType { get => _stateType; set => Set(() => StateType, ref _stateType, value); } - /// - /// - /// public User User { get => _user; @@ -137,9 +104,6 @@ public User User #region Constructors - /// - /// - /// public Inquiry() { } diff --git a/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/Entities/Product.Generated.cs b/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/Entities/Product.Generated.cs index 803f545..97dbfe9 100644 --- a/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/Entities/Product.Generated.cs +++ b/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/Entities/Product.Generated.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by EasyAF's Code Generators. -// Date Generated: 11/28/2025 7:08:07 PM +// Date Generated: 9/14/2026 3:48:07 AM // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -18,9 +18,6 @@ namespace EasyAFModel { - /// - /// - /// public partial class Product : DbObservableObject, IHasStatus, ICreatedAuditable, ICreatorTrackable, IUpdatedAuditable, IUpdaterTrackable { @@ -39,18 +36,12 @@ public partial class Product : DbObservableObject, IHasStatus #region Public Properties - /// - /// - /// public Guid Id { get => _id; set => Set(() => Id, ref _id, value); } - /// - /// - /// [StringLength(50)] public string DisplayName { @@ -58,54 +49,36 @@ public string DisplayName set => Set(() => DisplayName, ref _displayName, value); } - /// - /// - /// public Guid StatusTypeId { get => _statusTypeId; set => Set(() => StatusTypeId, ref _statusTypeId, value); } - /// - /// - /// public Guid CreatedById { get => _createdById; set => Set(() => CreatedById, ref _createdById, value); } - /// - /// - /// public DateTimeOffset DateCreated { get => _dateCreated; set => Set(() => DateCreated, ref _dateCreated, value); } - /// - /// - /// public Nullable UpdatedById { get => _updatedById; set => Set(() => UpdatedById, ref _updatedById, value); } - /// - /// - /// public Nullable DateUpdated { get => _dateUpdated; set => Set(() => DateUpdated, ref _dateUpdated, value); } - /// - /// - /// public ProductStatusType StatusType { get => _statusType; @@ -116,9 +89,6 @@ public ProductStatusType StatusType #region Constructors - /// - /// - /// public Product() { } diff --git a/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/Entities/User.Generated.cs b/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/Entities/User.Generated.cs index 1af0254..d317409 100644 --- a/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/Entities/User.Generated.cs +++ b/src/CloudNimble.EasyAF.Tests.CodeGen/Baselines/Entities/User.Generated.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by EasyAF's Code Generators. -// Date Generated: 11/28/2025 7:08:07 PM +// Date Generated: 9/14/2026 3:48:07 AM // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -18,9 +18,6 @@ namespace EasyAFModel { - /// - /// - /// public partial class User : DbObservableObject, IIdentifiable, ICreatedAuditable, ICreatorTrackable { @@ -37,18 +34,12 @@ public partial class User : DbObservableObject, IIdentifiable, ICreatedAud #region Public Properties - /// - /// - /// public Guid Id { get => _id; set => Set(() => Id, ref _id, value); } - /// - /// - /// [StringLength(30)] public string FirstName { @@ -56,9 +47,6 @@ public string FirstName set => Set(() => FirstName, ref _firstName, value); } - /// - /// - /// [StringLength(50)] public string LastName { @@ -66,27 +54,18 @@ public string LastName set => Set(() => LastName, ref _lastName, value); } - /// - /// - /// public Guid CreatedById { get => _createdById; set => Set(() => CreatedById, ref _createdById, value); } - /// - /// - /// public DateTimeOffset DateCreated { get => _dateCreated; set => Set(() => DateCreated, ref _dateCreated, value); } - /// - /// - /// public ObservableCollection Inquiries { get => _inquiries; @@ -97,9 +76,6 @@ public ObservableCollection Inquiries #region Constructors - /// - /// - /// public User() { } diff --git a/src/CloudNimble.EasyAF.Tests.CodeGen/DocumentationDiagnosticTests.cs b/src/CloudNimble.EasyAF.Tests.CodeGen/DocumentationDiagnosticTests.cs index f38f3b5..666bbc0 100644 --- a/src/CloudNimble.EasyAF.Tests.CodeGen/DocumentationDiagnosticTests.cs +++ b/src/CloudNimble.EasyAF.Tests.CodeGen/DocumentationDiagnosticTests.cs @@ -39,21 +39,23 @@ public void RestierTest_Documentation_ShouldBePopulated() userEntity.Should().NotBeNull("User entity should exist in the EDMX"); // Check entity-level documentation - var entityDoc = MetadataTools.Comment(userEntity.EntityType); + var entityDoc = MetadataTools.ToXmlDoc(userEntity.EntityType); Console.WriteLine($"User entity Documentation object: {userEntity.EntityType.Documentation}"); Console.WriteLine($"User entity Documentation.Summary: {userEntity.EntityType.Documentation?.Summary}"); - Console.WriteLine($"User entity documentation via MetadataTools: '{entityDoc}'"); - entityDoc.Should().NotBeEmpty("User entity should have documentation from YOUR LUMINARY! YOUR LIBERATOR! CLU!"); + Console.WriteLine($"User entity documentation via MetadataTools: '{entityDoc.Summary}'"); + entityDoc.HasSummary.Should().BeTrue("User entity should have documentation from YOUR LUMINARY! YOUR LIBERATOR! CLU!"); + entityDoc.Summary.Should().Contain("YOUR LUMINARY! YOUR LIBERATOR! CLU!"); // Check property-level documentation (EmailAddress has documentation) var emailProperty = userEntity.EntityType.Properties.FirstOrDefault(p => p.Name == "EmailAddress"); emailProperty.Should().NotBeNull("EmailAddress property should exist"); - var propertyDoc = MetadataTools.Comment(emailProperty); + var propertyDoc = MetadataTools.ToXmlDoc(emailProperty); Console.WriteLine($"EmailAddress Documentation object: {emailProperty.Documentation}"); Console.WriteLine($"EmailAddress Documentation.Summary: {emailProperty.Documentation?.Summary}"); - Console.WriteLine($"EmailAddress documentation via MetadataTools: '{propertyDoc}'"); - propertyDoc.Should().NotBeEmpty("EmailAddress property should have documentation from You'd better find this you POS."); + Console.WriteLine($"EmailAddress documentation via MetadataTools: '{propertyDoc.Summary}'"); + propertyDoc.HasSummary.Should().BeTrue("EmailAddress property should have documentation from You'd better find this you POS."); + propertyDoc.Summary.Should().Contain("You'd better find this you POS."); } /// @@ -136,29 +138,30 @@ public void InlineEdmx_WithDocumentation_ShouldBePopulated() userEntity.EntityType.Name.Should().Be("User"); // Check entity documentation - var entityDoc = MetadataTools.Comment(userEntity.EntityType); + var entityDoc = MetadataTools.ToXmlDoc(userEntity.EntityType); Console.WriteLine($"User entity Documentation object: {userEntity.EntityType.Documentation}"); Console.WriteLine($"User entity Documentation.Summary: {userEntity.EntityType.Documentation?.Summary}"); - Console.WriteLine($"User entity documentation via MetadataTools: '{entityDoc}'"); - entityDoc.Should().Be("A user in the system.", "Entity should have documentation"); + Console.WriteLine($"User entity documentation via MetadataTools: '{entityDoc.Summary}'"); + entityDoc.Summary.Should().Be("A user in the system.", "Entity should have documentation"); + entityDoc.HasRemarks.Should().BeFalse(); // Check property documentation var idProperty = userEntity.EntityType.Properties.FirstOrDefault(p => p.Name == "Id"); idProperty.Should().NotBeNull(); - var idDoc = MetadataTools.Comment(idProperty); + var idDoc = MetadataTools.ToXmlDoc(idProperty); Console.WriteLine($"Id Documentation object: {idProperty.Documentation}"); Console.WriteLine($"Id Documentation.Summary: {idProperty.Documentation?.Summary}"); - Console.WriteLine($"Id documentation via MetadataTools: '{idDoc}'"); - idDoc.Should().Be("The unique identifier.", "Id property should have documentation"); + Console.WriteLine($"Id documentation via MetadataTools: '{idDoc.Summary}'"); + idDoc.Summary.Should().Be("The unique identifier.", "Id property should have documentation"); var nameProperty = userEntity.EntityType.Properties.FirstOrDefault(p => p.Name == "Name"); nameProperty.Should().NotBeNull(); - var nameDoc = MetadataTools.Comment(nameProperty); + var nameDoc = MetadataTools.ToXmlDoc(nameProperty); Console.WriteLine($"Name Documentation object: {nameProperty.Documentation}"); Console.WriteLine($"Name Documentation.Summary: {nameProperty.Documentation?.Summary}"); - Console.WriteLine($"Name documentation via MetadataTools: '{nameDoc}'"); + Console.WriteLine($"Name documentation via MetadataTools: '{nameDoc.Summary}'"); // Note: Apostrophes are escaped for XML compatibility - nameDoc.Should().Be("The user's display name.", "Name property should have documentation (with escaped apostrophe)"); + nameDoc.Summary.Should().Be("The user's display name.", "Name property should have documentation (with escaped apostrophe)"); } /// @@ -178,22 +181,23 @@ public void RestierTest_ListAllDocumentation_Diagnostic() foreach (var entity in loader.Entities.OrderBy(e => e.EntityType.Name)) { - var entityDoc = MetadataTools.Comment(entity.EntityType); + var entityDoc = MetadataTools.ToXmlDoc(entity.EntityType); Console.WriteLine($"Entity: {entity.EntityType.Name}"); Console.WriteLine($" Documentation object: {entity.EntityType.Documentation}"); Console.WriteLine($" Documentation.Summary: {entity.EntityType.Documentation?.Summary ?? "(null)"}"); - Console.WriteLine($" MetadataTools.Comment: '{entityDoc}'"); + Console.WriteLine($" MetadataTools.ToXmlDoc.Summary: '{entityDoc.Summary}'"); + Console.WriteLine($" MetadataTools.ToXmlDoc.Remarks: '{entityDoc.Remarks}'"); Console.WriteLine(); foreach (var prop in entity.EntityType.Properties.OrderBy(p => p.Name)) { - var propDoc = MetadataTools.Comment(prop); - if (!string.IsNullOrEmpty(propDoc) || prop.Documentation != null) + var propDoc = MetadataTools.ToXmlDoc(prop); + if (!propDoc.IsEmpty || prop.Documentation != null) { Console.WriteLine($" Property: {prop.Name}"); Console.WriteLine($" Documentation object: {prop.Documentation}"); Console.WriteLine($" Documentation.Summary: {prop.Documentation?.Summary ?? "(null)"}"); - Console.WriteLine($" MetadataTools.Comment: '{propDoc}'"); + Console.WriteLine($" MetadataTools.ToXmlDoc.Summary: '{propDoc.Summary}'"); } } Console.WriteLine(); @@ -225,9 +229,10 @@ public void RestierTest_GeneratedCode_ShouldContainDocumentation() Console.WriteLine(generatedCode); Console.WriteLine("=== END GENERATED CODE ==="); - // Entity class documentation - generatedCode.Should().Contain("YOUR LUMINARY! YOUR LIBERATOR! CLU!", - "User entity documentation should appear in generated code"); + generatedCode.Should().Contain("/// "); + generatedCode.Should().Contain("/// YOUR LUMINARY! YOUR LIBERATOR! CLU!"); + generatedCode.Should().Contain("/// "); + generatedCode.Should().NotContain("/// "); // Property documentation - EmailAddress (apostrophe is escaped for XML) generatedCode.Should().Contain("You'd better find this you POS.", @@ -238,6 +243,215 @@ public void RestierTest_GeneratedCode_ShouldContainDocumentation() "Id property documentation should appear in generated code"); } + /// + /// Maps CSDL Summary → summary and LongDescription → remarks without collapsing either field. + /// + [TestMethod] + public void ToXmlDoc_WithSummaryAndLongDescription_ShouldMapToSummaryAndRemarks() + { + var docs = MetadataTools.ToXmlDoc(new System.Data.Entity.Core.Metadata.Edm.Documentation( + "A user in the system.", + "Longer explanation of the user entity.")); + + docs.Summary.Should().Be("A user in the system."); + docs.Remarks.Should().Be("Longer explanation of the user entity."); + docs.HasSummary.Should().BeTrue(); + docs.HasRemarks.Should().BeTrue(); + docs.IsEmpty.Should().BeFalse(); + } + + /// + /// LongDescription-only must not fall back into summary. + /// + [TestMethod] + public void ToXmlDoc_LongDescriptionOnly_ShouldNotFallBackToSummary() + { + var docs = MetadataTools.ToXmlDoc(new System.Data.Entity.Core.Metadata.Edm.Documentation( + "", + "Longer explanation of the user entity.")); + + docs.HasSummary.Should().BeFalse(); + docs.Remarks.Should().Be("Longer explanation of the user entity."); + } + + /// + /// Null / empty Documentation produces an empty comment (no XML doc block). + /// + [TestMethod] + public void ToXmlDoc_NullOrEmpty_ShouldBeEmpty() + { + MetadataTools.ToXmlDoc((System.Data.Entity.Core.Metadata.Edm.Documentation)null).IsEmpty.Should().BeTrue(); + MetadataTools.ToXmlDoc(new System.Data.Entity.Core.Metadata.Edm.Documentation("", "")).IsEmpty.Should().BeTrue(); + } + + /// + /// Generated C# writes both tags when the EDMX has Summary and LongDescription. + /// + [TestMethod] + public void InlineEdmx_WithSummaryAndLongDescription_ShouldEmitSummaryAndRemarks() + { + var edmx = """ + + + + + + + + + + + + + + + + + + + + + + A user in the system. + Longer explanation of the user entity. + + + + + + + The unique identifier. + Primary key for the user row. + + + + + + + + + + + + + + + + + + + + + + + + + + """; + + var loader = new EdmxLoader(); + loader.Load(edmx); + loader.EdmxSchemaErrors.Should().BeEmpty(); + + var userEntity = loader.Entities.First(); + var entityDoc = MetadataTools.ToXmlDoc(userEntity.EntityType); + entityDoc.Summary.Should().Be("A user in the system."); + entityDoc.Remarks.Should().Be("Longer explanation of the user entity."); + + var idDoc = MetadataTools.ToXmlDoc(userEntity.EntityType.Properties.First(p => p.Name == "Id")); + idDoc.Summary.Should().Be("The unique identifier."); + idDoc.Remarks.Should().Be("Primary key for the user row."); + + var generator = new EntityGenerator(new List(), "TestModel", userEntity); + generator.Generate(); + var generatedCode = generator.ToString(); + + generatedCode.Should().Contain("/// A user in the system."); + generatedCode.Should().Contain("/// Longer explanation of the user entity."); + generatedCode.Should().Contain("/// The unique identifier."); + generatedCode.Should().Contain("/// Primary key for the user row."); + generatedCode.Should().Contain("/// "); + generatedCode.Should().Contain("/// "); + } + + /// + /// LongDescription-only EDMX emits remarks and no summary. Missing Documentation emits no empty summary block. + /// + [TestMethod] + public void InlineEdmx_LongDescriptionOnlyAndMissingDocs_ShouldNotEmitEmptySummary() + { + var edmx = """ + + + + + + + + + + + + + + + + + + + + + + Longer explanation of the user entity. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """; + + var loader = new EdmxLoader(); + loader.Load(edmx); + loader.EdmxSchemaErrors.Should().BeEmpty(); + + var userEntity = loader.Entities.First(); + var entityDoc = MetadataTools.ToXmlDoc(userEntity.EntityType); + entityDoc.HasSummary.Should().BeFalse(); + entityDoc.Remarks.Should().Be("Longer explanation of the user entity."); + + var generator = new EntityGenerator(new List(), "TestModel", userEntity); + generator.Generate(); + var generatedCode = generator.ToString(); + + generatedCode.Should().Contain("/// "); + generatedCode.Should().Contain("/// Longer explanation of the user entity."); + generatedCode.Should().Contain("/// "); + generatedCode.Should().NotContain(""); + } + #region SanitizeXmlComment Tests /// diff --git a/src/CloudNimble.EasyAF.Tests.CodeGen/SimpleMessageBusGeneratorTests.cs b/src/CloudNimble.EasyAF.Tests.CodeGen/SimpleMessageBusGeneratorTests.cs index 7339601..4b16a5c 100644 --- a/src/CloudNimble.EasyAF.Tests.CodeGen/SimpleMessageBusGeneratorTests.cs +++ b/src/CloudNimble.EasyAF.Tests.CodeGen/SimpleMessageBusGeneratorTests.cs @@ -130,45 +130,6 @@ public void CustomNamespace() result.Should().Contain($"namespace {customNamespace}"); } - [TestMethod] - public void WriteSimpleMessageBusFiles() - { - var userEntity = EdmxLoader.Entities.FirstOrDefault(c => c.EntityType.Name == "User"); - var outputDir = Path.Combine(ProjectPath, @"Baselines\SimpleMessageBus"); - - // Generate base class - using (var baseGenerator = new SimpleMessageBusGenerator(null, EdmxLoader.ModelNamespace, userEntity, "Base")) - { - baseGenerator.Generate(); - var path = baseGenerator.WriteFile(outputDir); - File.Exists(path).Should().BeTrue(); - } - - // Generate created message - using (var createdGenerator = new SimpleMessageBusGenerator(null, EdmxLoader.ModelNamespace, userEntity, "Created")) - { - createdGenerator.Generate(); - var path = createdGenerator.WriteFile(outputDir); - File.Exists(path).Should().BeTrue(); - } - - // Generate updated message - using (var updatedGenerator = new SimpleMessageBusGenerator(null, EdmxLoader.ModelNamespace, userEntity, "Updated")) - { - updatedGenerator.Generate(); - var path = updatedGenerator.WriteFile(outputDir); - File.Exists(path).Should().BeTrue(); - } - - // Generate deleted message - using (var deletedGenerator = new SimpleMessageBusGenerator(null, EdmxLoader.ModelNamespace, userEntity, "Deleted")) - { - deletedGenerator.Generate(); - var path = deletedGenerator.WriteFile(outputDir); - File.Exists(path).Should().BeTrue(); - } - } - //[DataRow(ProjectPath)] //[TestMethod] [BreakdanceManifestGenerator] diff --git a/src/CloudNimble.EasyAF.Tests.Configuration/CloudNimble.EasyAF.Tests.Configuration.csproj b/src/CloudNimble.EasyAF.Tests.Configuration/CloudNimble.EasyAF.Tests.Configuration.csproj index 21f29db..cfaef8c 100644 --- a/src/CloudNimble.EasyAF.Tests.Configuration/CloudNimble.EasyAF.Tests.Configuration.csproj +++ b/src/CloudNimble.EasyAF.Tests.Configuration/CloudNimble.EasyAF.Tests.Configuration.csproj @@ -1,7 +1,7 @@ - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; false $(NoWarn);CA1822; @@ -10,18 +10,6 @@ - - - - - - - - - - - - diff --git a/src/CloudNimble.EasyAF.Tests.Core/CloudNimble.EasyAF.Tests.Core.csproj b/src/CloudNimble.EasyAF.Tests.Core/CloudNimble.EasyAF.Tests.Core.csproj index 457d2a2..9520b1e 100644 --- a/src/CloudNimble.EasyAF.Tests.Core/CloudNimble.EasyAF.Tests.Core.csproj +++ b/src/CloudNimble.EasyAF.Tests.Core/CloudNimble.EasyAF.Tests.Core.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; false diff --git a/src/CloudNimble.EasyAF.Tests.Core/IntervalTests.cs b/src/CloudNimble.EasyAF.Tests.Core/IntervalTests.cs index 144856a..3e2029c 100644 --- a/src/CloudNimble.EasyAF.Tests.Core/IntervalTests.cs +++ b/src/CloudNimble.EasyAF.Tests.Core/IntervalTests.cs @@ -51,7 +51,7 @@ public class IntervalTests public void PerMinuteTests(double input, IntervalType intervalType, double output) { var interval = new Interval(Convert.ToDecimal(input), intervalType); - Math.Round(interval.PerMinute(), 8).Should().Be(Convert.ToDecimal(output)); + Math.Round(interval.PerMinute(), 8).Should().Be(Math.Round(Convert.ToDecimal(output), 8)); } #endregion @@ -97,7 +97,7 @@ public void PerMinuteTests(double input, IntervalType intervalType, double outpu public void PerHourTests(double input, IntervalType intervalType, double output) { var interval = new Interval(Convert.ToDecimal(input), intervalType); - Math.Round(interval.PerHour(), 8).Should().Be(Convert.ToDecimal(output)); + Math.Round(interval.PerHour(), 8).Should().Be(Math.Round(Convert.ToDecimal(output), 8)); } #endregion @@ -143,7 +143,7 @@ public void PerHourTests(double input, IntervalType intervalType, double output) public void PerDayTests(double input, IntervalType intervalType, double output) { var interval = new Interval(Convert.ToDecimal(input), intervalType); - Math.Round(interval.PerDay(), 8).Should().Be(Convert.ToDecimal(output)); + Math.Round(interval.PerDay(), 8).Should().Be(Math.Round(Convert.ToDecimal(output), 8)); } #endregion @@ -189,7 +189,7 @@ public void PerDayTests(double input, IntervalType intervalType, double output) public void PerWeekTests(double input, IntervalType intervalType, double output) { var interval = new Interval(Convert.ToDecimal(input), intervalType); - Math.Round(interval.PerWeek(), 8).Should().Be(Convert.ToDecimal(output)); + Math.Round(interval.PerWeek(), 8).Should().Be(Math.Round(Convert.ToDecimal(output), 8)); } #endregion @@ -235,7 +235,7 @@ public void PerWeekTests(double input, IntervalType intervalType, double output) public void PerMonthTests(double input, IntervalType intervalType, double output) { var interval = new Interval(Convert.ToDecimal(input), intervalType); - Math.Round(interval.PerMonth(), 8).Should().Be(Convert.ToDecimal(output)); + Math.Round(interval.PerMonth(), 8).Should().Be(Math.Round(Convert.ToDecimal(output), 8)); } #endregion @@ -281,7 +281,7 @@ public void PerMonthTests(double input, IntervalType intervalType, double output public void PerYearTests(double input, IntervalType intervalType, double output) { var interval = new Interval(Convert.ToDecimal(input), intervalType); - Math.Round(interval.PerYear(), 8).Should().Be(Convert.ToDecimal(output)); + Math.Round(interval.PerYear(), 8).Should().Be(Math.Round(Convert.ToDecimal(output), 8)); } #endregion diff --git a/src/CloudNimble.EasyAF.Tests.Data.EF6/CloudNimble.EasyAF.Tests.Data.EF6.csproj b/src/CloudNimble.EasyAF.Tests.Data.EF6/CloudNimble.EasyAF.Tests.Data.EF6.csproj index 0875927..6f5eaf0 100644 --- a/src/CloudNimble.EasyAF.Tests.Data.EF6/CloudNimble.EasyAF.Tests.Data.EF6.csproj +++ b/src/CloudNimble.EasyAF.Tests.Data.EF6/CloudNimble.EasyAF.Tests.Data.EF6.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0 + net11.0;net10.0;net9.0;net8.0 diff --git a/src/CloudNimble.EasyAF.Tests.Data.EF6/EntityFramework6Tests.cs b/src/CloudNimble.EasyAF.Tests.Data.EF6/EntityFramework6Tests.cs index 8d31df6..4968816 100644 --- a/src/CloudNimble.EasyAF.Tests.Data.EF6/EntityFramework6Tests.cs +++ b/src/CloudNimble.EasyAF.Tests.Data.EF6/EntityFramework6Tests.cs @@ -9,6 +9,7 @@ namespace CloudNimble.EasyAF.Tests.Data.EF6 [TestClass] [TestCategory("RequiresDatabase")] + [DoNotParallelize] public class EntityFramework6Tests { diff --git a/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/CloudNimble.EasyAF.Tests.EFCoreToEdmx.csproj b/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/CloudNimble.EasyAF.Tests.EFCoreToEdmx.csproj index 2fdce3d..3f1abfc 100644 --- a/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/CloudNimble.EasyAF.Tests.EFCoreToEdmx.csproj +++ b/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/CloudNimble.EasyAF.Tests.EFCoreToEdmx.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0 + net11.0;net10.0;net9.0;net8.0 false $(NoWarn);CA1822;NU1701; bcb335b9-8bc0-43f0-b414-464196a34198 @@ -14,6 +14,14 @@ + + + + + + + + diff --git a/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/EasyAFLongDescriptionTests.cs b/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/EasyAFLongDescriptionTests.cs new file mode 100644 index 0000000..67beb12 --- /dev/null +++ b/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/EasyAFLongDescriptionTests.cs @@ -0,0 +1,180 @@ +using CloudNimble.EasyAF.EFCoreToEdmx; +using CloudNimble.EasyAF.EFCoreToEdmx.Models; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace CloudNimble.EasyAF.Tests.EFCoreToEdmx +{ + + [TestClass] + public class EasyAFLongDescriptionTests + { + + [TestMethod] + public void Apply_ShouldStampTableAndColumnLongDescriptionFromInMemoryCatalog() + { + var model = CreatePostsModel(); + var catalog = new Dictionary<(string Table, string Column), string> + { + [("posts", null)] = "ClipId is optional. When IsFullEpisode is true, metrics go to EpisodeMetrics.", + [("posts", "isfullepisode")] = "When true, metrics go to EpisodeMetrics, not PostMetrics." + }; + + EasyAFLongDescription.Apply(model, catalog); + + var entity = model.EntityTypes[0]; + entity.LongDescription.Should().Be("ClipId is optional. When IsFullEpisode is true, metrics go to EpisodeMetrics."); + entity.Properties.First(p => p.Name == "IsFullEpisode").LongDescription + .Should().Be("When true, metrics go to EpisodeMetrics, not PostMetrics."); + entity.Documentation.Should().Be("A single platform destination for content."); + } + + [TestMethod] + public void Apply_FromRows_ShouldBuildCatalogAndLookupByTable() + { + var model = CreatePostsModel(); + + EasyAFLongDescription.Apply(model, + [ + ("Posts", null, "ClipId is optional. When IsFullEpisode is true, metrics go to EpisodeMetrics."), + ("Posts", "IsFullEpisode", "When true, metrics go to EpisodeMetrics, not PostMetrics.") + ]); + + var entity = model.EntityTypes[0]; + entity.LongDescription.Should().Be("ClipId is optional. When IsFullEpisode is true, metrics go to EpisodeMetrics."); + entity.Properties.First(p => p.Name == "IsFullEpisode").LongDescription + .Should().Be("When true, metrics go to EpisodeMetrics, not PostMetrics."); + } + + [TestMethod] + public void PreserveExisting_ShouldFillEmptyLongDescriptionAndLeaveCatalogValues() + { + var model = CreatePostsModel(); + model.EntityTypes[0].LongDescription = "From catalog."; + + var existing = """ + + + + + + + + Hand-authored summary. + Hand-authored long description. + + + + Hand-authored column remarks. + + + + + + + + """; + + EasyAFLongDescription.PreserveExisting(model, existing); + + var entity = model.EntityTypes[0]; + entity.Documentation.Should().Be("A single platform destination for content.", "catalog Summary is already set"); + entity.LongDescription.Should().Be("From catalog.", "catalog LongDescription is already set"); + entity.Properties.First(p => p.Name == "IsFullEpisode").LongDescription + .Should().Be("Hand-authored column remarks.", "empty model field is filled from existing CSDL"); + } + + [TestMethod] + public void PreserveExisting_WithoutCatalogProperty_ShouldKeepHandAuthoredLongDescription() + { + var model = CreatePostsModel(); + + var existing = """ + + + + + + + + Hand-authored long description. + + + + + + + """; + + EasyAFLongDescription.PreserveExisting(model, existing); + + model.EntityTypes[0].LongDescription.Should().Be("Hand-authored long description."); + } + + [TestMethod] + public void Generate_AfterPreserveExisting_ShouldEmitHandAuthoredLongDescription() + { + var model = CreatePostsModel(); + var existing = """ + + + + + + + + Hand-authored long description. + + + + + + + """; + + EasyAFLongDescription.PreserveExisting(model, existing); + var xml = new EdmxXmlGenerator(model, CloudNimble.EasyAF.EFCoreToEdmx.DatabaseProviderType.SqlServer).Generate(); + var doc = XDocument.Parse(xml); + + var entity = doc.Descendants() + .First(x => x.Name.LocalName == "EntityType" && x.Attribute("Name")?.Value == "Post"); + var documentation = entity.Elements().First(x => x.Name.LocalName == "Documentation"); + documentation.Elements().First(x => x.Name.LocalName == "Summary").Value + .Should().Be("A single platform destination for content."); + documentation.Elements().First(x => x.Name.LocalName == "LongDescription").Value + .Should().Be("Hand-authored long description."); + } + + private static EdmxModel CreatePostsModel() + { + return new EdmxModel + { + Namespace = "Test", + ContainerName = "TestContext", + EntityTypes = + [ + new EdmxEntityType + { + Name = "Post", + Documentation = "A single platform destination for content.", + Properties = + [ + new EdmxProperty { Name = "Id", Type = "Guid", Nullable = false }, + new EdmxProperty { Name = "IsFullEpisode", Type = "Boolean", Nullable = false, StoreColumnName = "IsFullEpisode" } + ], + Keys = ["Id"] + } + ], + EntitySets = + [ + new EdmxEntitySet { Name = "Posts", EntityTypeName = "Post" } + ] + }; + } + + } + +} diff --git a/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/EdmxXmlGeneratorTests.cs b/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/EdmxXmlGeneratorTests.cs index 523e19b..8c157eb 100644 --- a/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/EdmxXmlGeneratorTests.cs +++ b/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/EdmxXmlGeneratorTests.cs @@ -845,6 +845,59 @@ public void Generate_WithoutEntityDocumentation_ShouldNotIncludeEmptyDocumentati documentation.Should().BeNull("Empty documentation elements should not be created"); } + /// + /// CSDL emits both Summary and LongDescription; SSDL keeps Summary only. + /// + [TestMethod] + public void Generate_WithSummaryAndLongDescription_ShouldEmitBothInConceptualModel() + { + var model = CreateTestEdmxModelWithEntityDocumentation(); + model.EntityTypes[0].LongDescription = "ClipId is optional. When IsFullEpisode is true, metrics go to EpisodeMetrics."; + model.EntityTypes[0].Properties.First(p => p.Name == "Name").LongDescription = "Display name used in the API."; + + var result = new EdmxXmlGenerator(model, CloudNimble.EasyAF.EFCoreToEdmx.DatabaseProviderType.SqlServer).Generate(); + var doc = XDocument.Parse(result); + + var conceptualEntity = doc.Descendants() + .First(x => x.Name.LocalName == "Schema" && x.Attribute("Namespace")?.Value == "TestNamespace") + .Descendants() + .First(x => x.Name.LocalName == "EntityType" && x.Attribute("Name")?.Value == "DocumentedEntity"); + + var documentation = conceptualEntity.Elements().First(x => x.Name.LocalName == "Documentation"); + documentation.Elements().First(x => x.Name.LocalName == "Summary").Value + .Should().Be("This entity represents a documented table with important business data"); + documentation.Elements().First(x => x.Name.LocalName == "LongDescription").Value + .Should().Be("ClipId is optional. When IsFullEpisode is true, metrics go to EpisodeMetrics."); + + var nameProperty = conceptualEntity.Elements().First(x => x.Name.LocalName == "Property" && x.Attribute("Name")?.Value == "Name"); + var propertyDocs = nameProperty.Elements().First(x => x.Name.LocalName == "Documentation"); + propertyDocs.Elements().First(x => x.Name.LocalName == "Summary").Value.Should().Be("The name property with its own documentation"); + propertyDocs.Elements().First(x => x.Name.LocalName == "LongDescription").Value.Should().Be("Display name used in the API."); + + var storageEntity = doc.Descendants() + .First(x => x.Name.LocalName == "Schema" && x.Attribute("Namespace")?.Value.Contains(".Store") == true) + .Descendants() + .First(x => x.Name.LocalName == "EntityType" && x.Attribute("Name")?.Value == "DocumentedEntities"); + var storageDocs = storageEntity.Elements().First(x => x.Name.LocalName == "Documentation"); + storageDocs.Elements().Any(x => x.Name.LocalName == "LongDescription").Should().BeFalse(); + } + + /// + /// Summary-only does not emit an empty LongDescription element. + /// + [TestMethod] + public void Generate_SummaryOnly_ShouldNotEmitEmptyLongDescription() + { + var model = CreateTestEdmxModelWithEntityDocumentation(); + var result = new EdmxXmlGenerator(model, CloudNimble.EasyAF.EFCoreToEdmx.DatabaseProviderType.SqlServer).Generate(); + var doc = XDocument.Parse(result); + + var conceptualEntity = doc.Descendants() + .First(x => x.Name.LocalName == "EntityType" && x.Attribute("Name")?.Value == "DocumentedEntity"); + var documentation = conceptualEntity.Elements().First(x => x.Name.LocalName == "Documentation"); + documentation.Elements().Any(x => x.Name.LocalName == "LongDescription").Should().BeFalse(); + } + #endregion #region Documentation Tests diff --git a/src/CloudNimble.EasyAF.Tests.Http.NewtonsoftJson/CloudNimble.EasyAF.Tests.Http.NewtonsoftJson.csproj b/src/CloudNimble.EasyAF.Tests.Http.NewtonsoftJson/CloudNimble.EasyAF.Tests.Http.NewtonsoftJson.csproj index dacb62c..8eb0bea 100644 --- a/src/CloudNimble.EasyAF.Tests.Http.NewtonsoftJson/CloudNimble.EasyAF.Tests.Http.NewtonsoftJson.csproj +++ b/src/CloudNimble.EasyAF.Tests.Http.NewtonsoftJson/CloudNimble.EasyAF.Tests.Http.NewtonsoftJson.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0;net48;net472; + net11.0;net10.0;net9.0;net8.0;net48;net472; $(DefineConstants)TRACE;NEWTONSOFT false @@ -15,19 +15,20 @@ + + + + - - - diff --git a/src/CloudNimble.EasyAF.Tests.Http.SystemTextJson/CloudNimble.EasyAF.Tests.Http.SystemTextJson.csproj b/src/CloudNimble.EasyAF.Tests.Http.SystemTextJson/CloudNimble.EasyAF.Tests.Http.SystemTextJson.csproj index 39c5aa6..2344952 100644 --- a/src/CloudNimble.EasyAF.Tests.Http.SystemTextJson/CloudNimble.EasyAF.Tests.Http.SystemTextJson.csproj +++ b/src/CloudNimble.EasyAF.Tests.Http.SystemTextJson/CloudNimble.EasyAF.Tests.Http.SystemTextJson.csproj @@ -1,7 +1,7 @@ - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; $(DefineConstants)TRACE;NEWTONSOFT false @@ -10,14 +10,20 @@ + + + + + + + + - - diff --git a/src/CloudNimble.EasyAF.Tests.Http/CloudNimble.EasyAF.Tests.Http.csproj b/src/CloudNimble.EasyAF.Tests.Http/CloudNimble.EasyAF.Tests.Http.csproj index 7b51877..8b2c42b 100644 --- a/src/CloudNimble.EasyAF.Tests.Http/CloudNimble.EasyAF.Tests.Http.csproj +++ b/src/CloudNimble.EasyAF.Tests.Http/CloudNimble.EasyAF.Tests.Http.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; false diff --git a/src/CloudNimble.EasyAF.Tests.MSBuild/CloudNimble.EasyAF.Tests.MSBuild.csproj b/src/CloudNimble.EasyAF.Tests.MSBuild/CloudNimble.EasyAF.Tests.MSBuild.csproj index 3ea20b5..9e86ecd 100644 --- a/src/CloudNimble.EasyAF.Tests.MSBuild/CloudNimble.EasyAF.Tests.MSBuild.csproj +++ b/src/CloudNimble.EasyAF.Tests.MSBuild/CloudNimble.EasyAF.Tests.MSBuild.csproj @@ -1,7 +1,7 @@ - net10.0;net9.0;net8.0 + net11.0;net10.0;net9.0;net8.0 false true true diff --git a/src/CloudNimble.EasyAF.Tests.ODataClient/ApiClientTests.cs b/src/CloudNimble.EasyAF.Tests.ODataClient/ApiClientTests.cs index c58f2be..db60ab1 100644 --- a/src/CloudNimble.EasyAF.Tests.ODataClient/ApiClientTests.cs +++ b/src/CloudNimble.EasyAF.Tests.ODataClient/ApiClientTests.cs @@ -36,7 +36,7 @@ public class ApiClientTests : RestierBreakdanceTestBase /// public ApiClientTests() { - TestHostBuilder.ConfigureServices(services => + TestHostBuilder.ConfigureServices((_, services) => { // we need to use a delegate function here because the TestServer does not exist at the time the services are configured. services.AddSingleton(new FakeHttpClientFactory(() => diff --git a/src/CloudNimble.EasyAF.Tests.ODataClient/CloudNimble.EasyAF.Tests.ODataClient.csproj b/src/CloudNimble.EasyAF.Tests.ODataClient/CloudNimble.EasyAF.Tests.ODataClient.csproj index 1a93092..5506dc3 100644 --- a/src/CloudNimble.EasyAF.Tests.ODataClient/CloudNimble.EasyAF.Tests.ODataClient.csproj +++ b/src/CloudNimble.EasyAF.Tests.ODataClient/CloudNimble.EasyAF.Tests.ODataClient.csproj @@ -1,26 +1,33 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; false - - + + + + + + + + + + - @@ -29,7 +36,6 @@ - @@ -38,7 +44,6 @@ - diff --git a/src/CloudNimble.EasyAF.Tests.Restier/CloudNimble.EasyAF.Tests.Restier.csproj b/src/CloudNimble.EasyAF.Tests.Restier/CloudNimble.EasyAF.Tests.Restier.csproj index 52db80d..39335c9 100644 --- a/src/CloudNimble.EasyAF.Tests.Restier/CloudNimble.EasyAF.Tests.Restier.csproj +++ b/src/CloudNimble.EasyAF.Tests.Restier/CloudNimble.EasyAF.Tests.Restier.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; false $(NoWarn);CS1705 @@ -17,6 +17,11 @@ + + + + + diff --git a/src/CloudNimble.EasyAF.Tests.Shared/CloudNimble.EasyAF.Tests.Shared.csproj b/src/CloudNimble.EasyAF.Tests.Shared/CloudNimble.EasyAF.Tests.Shared.csproj index 5f8a023..81f3fed 100644 --- a/src/CloudNimble.EasyAF.Tests.Shared/CloudNimble.EasyAF.Tests.Shared.csproj +++ b/src/CloudNimble.EasyAF.Tests.Shared/CloudNimble.EasyAF.Tests.Shared.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; false $(NoWarn);NU1705 diff --git a/src/CloudNimble.EasyAF.Tests.Tools/CloudNimble.EasyAF.Tests.Tools.csproj b/src/CloudNimble.EasyAF.Tests.Tools/CloudNimble.EasyAF.Tests.Tools.csproj index ced947b..b8af353 100644 --- a/src/CloudNimble.EasyAF.Tests.Tools/CloudNimble.EasyAF.Tests.Tools.csproj +++ b/src/CloudNimble.EasyAF.Tests.Tools/CloudNimble.EasyAF.Tests.Tools.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0 + net11.0;net10.0;net9.0;net8.0 false $(NoWarn);NU1701;NU1608 true diff --git a/src/CloudNimble.EasyAF.Tools/CloudNimble.EasyAF.Tools.csproj b/src/CloudNimble.EasyAF.Tools/CloudNimble.EasyAF.Tools.csproj index 791a336..46e0651 100644 --- a/src/CloudNimble.EasyAF.Tools/CloudNimble.EasyAF.Tools.csproj +++ b/src/CloudNimble.EasyAF.Tools/CloudNimble.EasyAF.Tools.csproj @@ -8,7 +8,7 @@ dotnet-easyaf Exe - net10.0;net9.0;net8.0; + net11.0;net10.0;net9.0;net8.0; $(DocumentationFile)\$(AssemblyName).xml $(NoWarn);CA1822;NU1701;NU1608; false @@ -23,6 +23,12 @@ + + + + + + diff --git a/src/CloudNimble.EasyAF.XmlDocumentation/CloudNimble.EasyAF.XmlDocumentation.csproj b/src/CloudNimble.EasyAF.XmlDocumentation/CloudNimble.EasyAF.XmlDocumentation.csproj index 823f8bc..e239024 100644 --- a/src/CloudNimble.EasyAF.XmlDocumentation/CloudNimble.EasyAF.XmlDocumentation.csproj +++ b/src/CloudNimble.EasyAF.XmlDocumentation/CloudNimble.EasyAF.XmlDocumentation.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0;netstandard2.0; + net11.0;net10.0;net9.0;net8.0;netstandard2.0; $(DocumentationFile)\$(AssemblyName).xml $(NoWarn);CA1822; diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 20c5485..dbb7c51 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -8,7 +8,11 @@ true true true - true + true + $([System.Text.RegularExpressions.Regex]::Match('$(TargetFramework)', '^net(\d+)\.').Groups[1].Value) + + 11.*-* + 10.*-* true true @@ -37,7 +41,7 @@ CloudNimble CloudNimble, Inc. CloudNimble - Copyright © 2020-2025 CloudNimble, Inc. All rights reserved. + Copyright © 2020-2026 CloudNimble, Inc. All rights reserved. en-US MIT readme.md @@ -82,7 +86,7 @@ - net10.0;net9.0;net8.0 + net11.0;net10.0;net9.0;net8.0 @@ -133,10 +137,10 @@ - + diff --git a/src/global.json b/src/global.json index 8d3ff85..62939fe 100644 --- a/src/global.json +++ b/src/global.json @@ -1,9 +1,10 @@ { "sdk": { - "version": "10.0.103", - "rollForward": "latestPatch" + "version": "11.0.100-rc.1.26425.128", + "rollForward": "latestFeature", + "allowPrerelease": true }, "test": { "runner": "Microsoft.Testing.Platform" } -} \ No newline at end of file +} From 3ca71bb68e56703763d760e77df1548399cd0e46 Mon Sep 17 00:00:00 2001 From: Robert McLaws <1657085+robertmclaws@users.noreply.github.com> Date: Mon, 14 Sep 2026 04:12:51 -0400 Subject: [PATCH 4/9] Z.EntityFramework.EF6 reference update. --- .../CloudNimble.EasyAF.Business.EF6.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CloudNimble.EasyAF.Business/CloudNimble.EasyAF.Business.EF6.csproj b/src/CloudNimble.EasyAF.Business/CloudNimble.EasyAF.Business.EF6.csproj index df9949b..81a5742 100644 --- a/src/CloudNimble.EasyAF.Business/CloudNimble.EasyAF.Business.EF6.csproj +++ b/src/CloudNimble.EasyAF.Business/CloudNimble.EasyAF.Business.EF6.csproj @@ -9,7 +9,7 @@ - + From 3cd2c111a4b1c0976be4c91d3ea10d3f863eace3 Mon Sep 17 00:00:00 2001 From: Robert McLaws <1657085+robertmclaws@users.noreply.github.com> Date: Mon, 14 Sep 2026 04:21:38 -0400 Subject: [PATCH 5/9] Install .NET 11 RC1 on GitHub Actions. Use channel 11.0.1xx with quality preview so CI gets the 11.0.100-rc.1 SDK band. Keep 8/9/10 GA installs for multi-targeting. --- .github/workflows/build-and-deploy.yml | 20 +++++++++++--------- .github/workflows/pr-validation.yml | 20 +++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 3388825..96036bc 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -24,7 +24,7 @@ permissions: id-token: write env: - DOTNET_VERSION: '10.0.x' + DOTNET_VERSION: '11.0.x' SOLUTION_FILE: 'src/CloudNimble.EasyAF.slnx' jobs: @@ -72,17 +72,19 @@ jobs: shell: pwsh run: | Write-Host "Installing .NET versions..." - $versions = @("8.0", "9.0", "10.0") - foreach ($version in $versions) { - Write-Host "Installing .NET $version..." - Invoke-WebRequest -Uri "https://dot.net/v1/dotnet-install.ps1" -OutFile "dotnet-install.ps1" - ./dotnet-install.ps1 -Channel $version -InstallDir "$env:ProgramFiles\dotnet" + Invoke-WebRequest -Uri "https://dot.net/v1/dotnet-install.ps1" -OutFile "dotnet-install.ps1" + $installDir = "$env:ProgramFiles\dotnet" + + foreach ($channel in @("8.0", "9.0", "10.0")) { + Write-Host "Installing .NET $channel..." + ./dotnet-install.ps1 -Channel $channel -InstallDir $installDir } - # Add to PATH for this job - echo "$env:ProgramFiles\dotnet" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + # 11.0.1xx is the SDK feature band that contains 11.0.100-rc.1 + Write-Host "Installing .NET 11.0.1xx (preview/RC)..." + ./dotnet-install.ps1 -Channel 11.0.1xx -Quality preview -InstallDir $installDir - # Verify installation + echo $installDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append dotnet --list-sdks - name: Get version variables diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 1fa630c..94e1a73 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -14,7 +14,7 @@ permissions: actions: write env: - DOTNET_VERSION: '10.0.x' + DOTNET_VERSION: '11.0.x' SOLUTION_FILE: 'src/CloudNimble.EasyAF.slnx' jobs: @@ -60,17 +60,19 @@ jobs: shell: pwsh run: | Write-Host "Installing .NET versions..." - $versions = @("8.0", "9.0", "10.0") - foreach ($version in $versions) { - Write-Host "Installing .NET $version..." - Invoke-WebRequest -Uri "https://dot.net/v1/dotnet-install.ps1" -OutFile "dotnet-install.ps1" - ./dotnet-install.ps1 -Channel $version -InstallDir "$env:ProgramFiles\dotnet" + Invoke-WebRequest -Uri "https://dot.net/v1/dotnet-install.ps1" -OutFile "dotnet-install.ps1" + $installDir = "$env:ProgramFiles\dotnet" + + foreach ($channel in @("8.0", "9.0", "10.0")) { + Write-Host "Installing .NET $channel..." + ./dotnet-install.ps1 -Channel $channel -InstallDir $installDir } - # Add to PATH for this job - echo "$env:ProgramFiles\dotnet" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + # 11.0.1xx is the SDK feature band that contains 11.0.100-rc.1 + Write-Host "Installing .NET 11.0.1xx (preview/RC)..." + ./dotnet-install.ps1 -Channel 11.0.1xx -Quality preview -InstallDir $installDir - # Verify installation + echo $installDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append dotnet --list-sdks - name: Get version variables From c6b6aaed8f3d3ca3ae7e852b0c54ca6b0ddf662c Mon Sep 17 00:00:00 2001 From: Robert McLaws <1657085+robertmclaws@users.noreply.github.com> Date: Mon, 14 Sep 2026 04:41:21 -0400 Subject: [PATCH 6/9] Basic Readme's for now. --- .../readme.md | 29 +++++++++++++- .../readme.md | 40 +++++++++++++++++++ src/CloudNimble.EasyAF.Business/readme.md | 38 ++++++++++++++++++ src/CloudNimble.EasyAF.CodeGen/readme.md | 30 ++++++++++++++ .../readme.md | 36 +++++++++++++++++ src/CloudNimble.EasyAF.Core/readme.md | 37 +++++++++++++++++ src/CloudNimble.EasyAF.Data.EF6/readme.md | 28 +++++++++++++ src/CloudNimble.EasyAF.Data.EFCore/readme.md | 29 ++++++++++++++ src/CloudNimble.EasyAF.EFCoreToEdmx/readme.md | 29 ++++++++++++++ .../readme.md | 27 +++++++++++++ src/CloudNimble.EasyAF.Edmx/readme.md | 24 +++++++++++ .../readme.md | 29 ++++++++++++++ .../readme.md | 28 +++++++++++++ .../readme.md | 26 ++++++++++++ src/CloudNimble.EasyAF.Http/readme.md | 31 ++++++++++++++ src/CloudNimble.EasyAF.MSBuild/readme.md | 30 ++++++++++++++ .../readme.md | 29 ++++++++++++++ src/CloudNimble.EasyAF.ODataClient/readme.md | 31 ++++++++++++++ .../readme.md | 25 ++++++++++++ src/CloudNimble.EasyAF.Restier.EF6/readme.md | 36 +++++++++++++++++ .../readme.md | 36 +++++++++++++++++ src/CloudNimble.EasyAF.Restier/readme.md | 26 ++++++++++++ src/CloudNimble.EasyAF.Tools/readme.md | 37 +++++++++++++++++ .../readme.md | 27 +++++++++++++ 24 files changed, 737 insertions(+), 1 deletion(-) create mode 100644 src/CloudNimble.EasyAF.Business.EFCore/readme.md create mode 100644 src/CloudNimble.EasyAF.Business/readme.md create mode 100644 src/CloudNimble.EasyAF.CodeGen/readme.md create mode 100644 src/CloudNimble.EasyAF.Configuration/readme.md create mode 100644 src/CloudNimble.EasyAF.Core/readme.md create mode 100644 src/CloudNimble.EasyAF.Data.EF6/readme.md create mode 100644 src/CloudNimble.EasyAF.Data.EFCore/readme.md create mode 100644 src/CloudNimble.EasyAF.EFCoreToEdmx/readme.md create mode 100644 src/CloudNimble.EasyAF.Edmx.InMemoryDb/readme.md create mode 100644 src/CloudNimble.EasyAF.Edmx/readme.md create mode 100644 src/CloudNimble.EasyAF.Extensions.Collections/readme.md create mode 100644 src/CloudNimble.EasyAF.Http.NewtonsoftJson/readme.md create mode 100644 src/CloudNimble.EasyAF.Http.SystemTextJson/readme.md create mode 100644 src/CloudNimble.EasyAF.Http/readme.md create mode 100644 src/CloudNimble.EasyAF.MSBuild/readme.md create mode 100644 src/CloudNimble.EasyAF.NewtonsoftJson.Compatibility/readme.md create mode 100644 src/CloudNimble.EasyAF.ODataClient/readme.md create mode 100644 src/CloudNimble.EasyAF.Restier.Breakdance/readme.md create mode 100644 src/CloudNimble.EasyAF.Restier.EF6/readme.md create mode 100644 src/CloudNimble.EasyAF.Restier.EFCore/readme.md create mode 100644 src/CloudNimble.EasyAF.Restier/readme.md create mode 100644 src/CloudNimble.EasyAF.Tools/readme.md create mode 100644 src/CloudNimble.EasyAF.XmlDocumentation/readme.md diff --git a/src/CloudNimble.EasyAF.Analyzers.EF6/readme.md b/src/CloudNimble.EasyAF.Analyzers.EF6/readme.md index f3db74b..17ec92b 100644 --- a/src/CloudNimble.EasyAF.Analyzers.EF6/readme.md +++ b/src/CloudNimble.EasyAF.Analyzers.EF6/readme.md @@ -1 +1,28 @@ -# EasyAF Analyzers & Generators \ No newline at end of file +# EasyAF.Analyzers.EF6 + +Roslyn source generators for EasyAF EF6 models (entities, DbContext, managers, Restier APIs). + +## Install + +```bash +dotnet add package EasyAF.Analyzers.EF6 +``` + +The package ships as an analyzer. Adding the reference is enough; you do not call the generators from application code. + +## Usage + +Point the generator at your EDMX (via the EasyAF MSBuild props the package imports) and rebuild. Generated C# is written as analyzer output. + +Prefer `dotnet easyaf code generate` when you want files on disk instead of compiler-generated sources. + +Requires an EF6 EDMX in the consuming project. For EF Core, generate EDMX with `EasyAF.EFCoreToEdmx` first. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Business.EFCore/readme.md b/src/CloudNimble.EasyAF.Business.EFCore/readme.md new file mode 100644 index 0000000..5f079d7 --- /dev/null +++ b/src/CloudNimble.EasyAF.Business.EFCore/readme.md @@ -0,0 +1,40 @@ +# EasyAF.Business.EFCore + +Entity managers for Entity Framework Core: CRUD, audit fields, and lifecycle hooks. + +## Install + +```bash +dotnet add package EasyAF.Business.EFCore +``` + +## Usage + +```csharp +public class UserManager : EntityManager +{ + public UserManager(MyDbContext context, IMessagePublisher publisher) + : base(context, publisher) { } + + public override async Task OnInsertingAsync(User entity) + { + await base.OnInsertingAsync(entity); // audit fields + entity.IsActive = true; + } +} +``` + +Also includes `IdentifiableEntityManager`, `StatusEntityManager`, and `StateMachineEntityManager`. For EF6, use `EasyAF.Business.EF6`. + +On .NET 11, `DirectUpdate` / `DirectDelete` are stubbed until Z.EntityFramework.Plus ships an EF Core 11 package. + +Requires `EasyAF.Data.EFCore`. + +## Documentation + +- Docs: https://easyaf.dev/guides/business-layer +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Business/readme.md b/src/CloudNimble.EasyAF.Business/readme.md new file mode 100644 index 0000000..f1f2d39 --- /dev/null +++ b/src/CloudNimble.EasyAF.Business/readme.md @@ -0,0 +1,38 @@ +# EasyAF.Business.EF6 + +Entity managers for Entity Framework 6: CRUD, audit fields, and lifecycle hooks. + +## Install + +```bash +dotnet add package EasyAF.Business.EF6 +``` + +## Usage + +```csharp +public class UserManager : EntityManager +{ + public UserManager(MyDbContext context, IMessagePublisher publisher) + : base(context, publisher) { } + + public override async Task OnInsertingAsync(User entity) + { + await base.OnInsertingAsync(entity); // audit fields + entity.IsActive = true; + } +} +``` + +Also includes `IdentifiableEntityManager`, `StatusEntityManager`, and `StateMachineEntityManager`. For EF Core, use `EasyAF.Business.EFCore`. + +Requires `EasyAF.Data.EF6`. + +## Documentation + +- Docs: https://easyaf.dev/guides/business-layer +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.CodeGen/readme.md b/src/CloudNimble.EasyAF.CodeGen/readme.md new file mode 100644 index 0000000..80f18fa --- /dev/null +++ b/src/CloudNimble.EasyAF.CodeGen/readme.md @@ -0,0 +1,30 @@ +# EasyAF.CodeGen + +EDMX-driven C# generators (entities, DbContext, managers, interceptors, Restier APIs, SimpleMessageBus messages). + +## Install + +```bash +dotnet add package EasyAF.CodeGen +``` + +This package is a development dependency. Typical consumers are `EasyAF.Analyzers.EF6` and the `EasyAF` CLI (`dotnet easyaf code generate`). + +## Usage + +```csharp +using var generator = new EntityGenerator(extraUsings, modelNamespace, entity); +generator.Generate(); +var csharp = generator.ToString(); +``` + +Load the EDMX with `EdmxLoader` first. Prefer the CLI or Roslyn analyzer rather than calling generators from application code. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Configuration/readme.md b/src/CloudNimble.EasyAF.Configuration/readme.md new file mode 100644 index 0000000..0e8f0d1 --- /dev/null +++ b/src/CloudNimble.EasyAF.Configuration/readme.md @@ -0,0 +1,36 @@ +# EasyAF.Configuration + +Binds app settings (API root, app root, HTTP handler mode) into a typed configuration object for EasyAF apps. + +## Install + +```bash +dotnet add package EasyAF.Configuration +``` + +## Usage + +```csharp +builder.Services.AddConfigurationBase(builder.Configuration, "AppSettings"); +``` + +```json +{ + "AppSettings": { + "ApiRoot": "https://api.example.com", + "AppRoot": "https://app.example.com", + "HttpHandlerMode": "Add" + } +} +``` + +Use `ConfigurationPlusAdminBase` when the app also needs an admin API client. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Core/readme.md b/src/CloudNimble.EasyAF.Core/readme.md new file mode 100644 index 0000000..2b9c95f --- /dev/null +++ b/src/CloudNimble.EasyAF.Core/readme.md @@ -0,0 +1,37 @@ +# EasyAF.Core + +Observable objects, identity, intervals, and shared primitives for EasyAF applications. + +## Install + +```bash +dotnet add package EasyAF.Core +``` + +## Usage + +```csharp +using CloudNimble.EasyAF.Core; + +public class Person : EasyObservableObject +{ + private string _name; + + public string Name + { + get => _name; + set => Set(nameof(Name), ref _name, value); + } +} +``` + +`EasyObservableObject` implements `INotifyPropertyChanged` with strongly typed `Set` helpers. The package also includes `IIdentifiable`, audit interfaces, `Interval` / `MoneyInterval`, and `Ensure` argument guards. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Data.EF6/readme.md b/src/CloudNimble.EasyAF.Data.EF6/readme.md new file mode 100644 index 0000000..c94ce43 --- /dev/null +++ b/src/CloudNimble.EasyAF.Data.EF6/readme.md @@ -0,0 +1,28 @@ +# EasyAF.Data.EF6 + +Entity Framework 6 provider configuration for SQL Server / Azure SQL using Microsoft.Data.SqlClient. + +## Install + +```bash +dotnet add package EasyAF.Data.EF6 +``` + +## Usage + +```csharp +DbConfiguration.SetConfiguration(new EasyAFSqlAzureConfiguration()); + +using var context = new MyDbContext(connectionString); +``` + +`EasyAFSqlAzureConfiguration` registers `Microsoft.Data.SqlClient`, EF6 SQL services, and `MicrosoftSqlAzureExecutionStrategy`. Optional Azure AD token support is in `AzureActiveDirectorySqlAuthProvider`. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Data.EFCore/readme.md b/src/CloudNimble.EasyAF.Data.EFCore/readme.md new file mode 100644 index 0000000..cf06716 --- /dev/null +++ b/src/CloudNimble.EasyAF.Data.EFCore/readme.md @@ -0,0 +1,29 @@ +# EasyAF.Data.EFCore + +Entity Framework Core helpers for EasyAF entities (ignore change-tracking fields on `DbObservableObject`). + +## Install + +```bash +dotnet add package EasyAF.Data.EFCore +``` + +## Usage + +```csharp +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + modelBuilder.Entity().IgnoreTrackingFields(); +} +``` + +`IgnoreTrackingFields` excludes `IsChanged`, `IsGraphChanged`, `ShouldTrackChanges`, and `OriginalValues` from the store model. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.EFCoreToEdmx/readme.md b/src/CloudNimble.EasyAF.EFCoreToEdmx/readme.md new file mode 100644 index 0000000..4e55f9b --- /dev/null +++ b/src/CloudNimble.EasyAF.EFCoreToEdmx/readme.md @@ -0,0 +1,29 @@ +# EasyAF.EFCoreToEdmx + +Converts an EF Core `DbContext` (or a live database) into EDMX for Restier and EasyAF code generation. + +## Install + +```bash +dotnet add package EasyAF.EFCoreToEdmx +``` + +## Usage + +```csharp +var converter = new EdmxConverter(); +await converter.ConvertToEdmxFileAsync(dbContext, "Model.edmx"); +``` + +CSDL `Summary` comes from EF comments / `MS_Description`. SQL Server `EasyAF_LongDescription` extended properties map to CSDL `LongDescription`. + +The `EasyAF` CLI (`dotnet easyaf database generate`) is the usual entry point. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Edmx.InMemoryDb/readme.md b/src/CloudNimble.EasyAF.Edmx.InMemoryDb/readme.md new file mode 100644 index 0000000..990330b --- /dev/null +++ b/src/CloudNimble.EasyAF.Edmx.InMemoryDb/readme.md @@ -0,0 +1,27 @@ +# EasyAF.Edmx.InMemoryDb + +In-memory EF6 provider (Effort-style) for EasyAF EDMX / ObjectContext tests. + +## Install + +```bash +dotnet add package EasyAF.Edmx.InMemoryDb +``` + +## Usage + +```csharp +var connection = EntityConnectionFactory.CreateTransient("name=MyEntities"); +using var context = new MyEntities(connection); +``` + +Use this when tests should not hit SQL Server. Pair with `EasyAF.Edmx`. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Edmx/readme.md b/src/CloudNimble.EasyAF.Edmx/readme.md new file mode 100644 index 0000000..4aa08f7 --- /dev/null +++ b/src/CloudNimble.EasyAF.Edmx/readme.md @@ -0,0 +1,24 @@ +# EasyAF.Edmx + +Portable Entity Framework 6 EDMX / ObjectContext runtime used by EasyAF code generation and EF6 apps. + +## Install + +```bash +dotnet add package EasyAF.Edmx +``` + +## Usage + +This is the netstandard2.0 EF6 stack EasyAF uses to load EDMX metadata and run EF6 on modern TFMs. Application projects usually take `EasyAF.Data.EF6` and `EasyAF.Business.EF6` instead of referencing this package directly. + +For in-memory tests, add `EasyAF.Edmx.InMemoryDb`. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Extensions.Collections/readme.md b/src/CloudNimble.EasyAF.Extensions.Collections/readme.md new file mode 100644 index 0000000..8b96db9 --- /dev/null +++ b/src/CloudNimble.EasyAF.Extensions.Collections/readme.md @@ -0,0 +1,29 @@ +# EasyAF.Extensions.Collections + +Bulk add/insert/remove for `Collection` and `ObservableCollection` (C# 14 extension members). + +## Install + +```bash +dotnet add package EasyAF.Extensions.Collections +``` + +## Usage + +```csharp +var items = new ObservableCollection(); +items.AddRange(["alpha", "beta", "gamma"]); +``` + +`ObservableCollection` overloads raise a single change notification. `Collection` overloads call `InsertItem` / `RemoveItem` so subclass validation still runs. + +Targets .NET 10. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Http.NewtonsoftJson/readme.md b/src/CloudNimble.EasyAF.Http.NewtonsoftJson/readme.md new file mode 100644 index 0000000..d768b52 --- /dev/null +++ b/src/CloudNimble.EasyAF.Http.NewtonsoftJson/readme.md @@ -0,0 +1,28 @@ +# EasyAF.Http.NewtonsoftJson + +`HttpResponseMessage` JSON helpers using Newtonsoft.Json. + +## Install + +```bash +dotnet add package EasyAF.Http.NewtonsoftJson +``` + +## Usage + +```csharp +var (products, error) = await response.DeserializeResponseAsync>(); +``` + +Use this package with `EasyAF.Http` when the app serializes with Newtonsoft.Json. For System.Text.Json, use `EasyAF.Http.SystemTextJson` instead. + +Depends on `EasyAF.NewtonsoftJson.Compatibility` so `[JsonPropertyName]` / `[JsonIgnore]` from System.Text.Json still apply. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Http.SystemTextJson/readme.md b/src/CloudNimble.EasyAF.Http.SystemTextJson/readme.md new file mode 100644 index 0000000..2228067 --- /dev/null +++ b/src/CloudNimble.EasyAF.Http.SystemTextJson/readme.md @@ -0,0 +1,26 @@ +# EasyAF.Http.SystemTextJson + +`HttpResponseMessage` JSON helpers using System.Text.Json. + +## Install + +```bash +dotnet add package EasyAF.Http.SystemTextJson +``` + +## Usage + +```csharp +var (products, error) = await response.DeserializeResponseAsync>(); +``` + +Use this package with `EasyAF.Http` when the app serializes with System.Text.Json. For Newtonsoft.Json, use `EasyAF.Http.NewtonsoftJson` instead. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Http/readme.md b/src/CloudNimble.EasyAF.Http/readme.md new file mode 100644 index 0000000..036f4d0 --- /dev/null +++ b/src/CloudNimble.EasyAF.Http/readme.md @@ -0,0 +1,31 @@ +# EasyAF.Http + +HttpClient factory helpers, OData v4 response types, and URI builders for EasyAF APIs. + +## Install + +```bash +dotnet add package EasyAF.Http +``` + +## Usage + +```csharp +services.AddHttpClients(config); +``` + +```csharp +var uri = new Uri("https://api.example.com/Products") + .ToODataUri(filter: "IsActive eq true", top: 20, orderby: "Name"); +``` + +Add `EasyAF.Http.SystemTextJson` or `EasyAF.Http.NewtonsoftJson` for JSON deserialization of `HttpResponseMessage`. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.MSBuild/readme.md b/src/CloudNimble.EasyAF.MSBuild/readme.md new file mode 100644 index 0000000..33cc69d --- /dev/null +++ b/src/CloudNimble.EasyAF.MSBuild/readme.md @@ -0,0 +1,30 @@ +# EasyAF.MSBuild + +Load, edit, and save `.csproj` / `Directory.Build.props` files while keeping original formatting. + +## Install + +```bash +dotnet add package EasyAF.MSBuild +``` + +## Usage + +```csharp +MSBuildProjectManager.EnsureMSBuildRegistered(); + +var manager = new MSBuildProjectManager("MyApp.csproj").Load(); +manager.SetProperty("Nullable", "enable"); +manager.Save(); +``` + +Call `EnsureMSBuildRegistered()` before any MSBuild API. Used by the `EasyAF` CLI. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.NewtonsoftJson.Compatibility/readme.md b/src/CloudNimble.EasyAF.NewtonsoftJson.Compatibility/readme.md new file mode 100644 index 0000000..82c960d --- /dev/null +++ b/src/CloudNimble.EasyAF.NewtonsoftJson.Compatibility/readme.md @@ -0,0 +1,29 @@ +# EasyAF.NewtonsoftJson.Compatibility + +Makes Newtonsoft.Json honor System.Text.Json serialization attributes. + +## Install + +```bash +dotnet add package EasyAF.NewtonsoftJson.Compatibility +``` + +## Usage + +```csharp +var settings = new JsonSerializerSettings +{ + ContractResolver = new SystemTextJsonContractResolver() +}; +``` + +Types decorated with `System.Text.Json.Serialization.JsonPropertyName` and `JsonIgnore` then serialize correctly under Newtonsoft.Json. `EasyAF.Http.NewtonsoftJson` uses this resolver automatically. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.ODataClient/readme.md b/src/CloudNimble.EasyAF.ODataClient/readme.md new file mode 100644 index 0000000..e1ea0d5 --- /dev/null +++ b/src/CloudNimble.EasyAF.ODataClient/readme.md @@ -0,0 +1,31 @@ +# EasyAF.ODataClient + +A preconfigured Simple.OData.Client v4 client that uses EasyAF HttpClient and configuration. + +## Install + +```bash +dotnet add package EasyAF.ODataClient +``` + +## Usage + +```csharp +services.AddSingleton(); + +var client = serviceProvider.GetRequiredService(); +var products = await client.For("Products").FindEntriesAsync(); +``` + +`ApiClient` is constructed from `IHttpClientFactory` and `ConfigurationBase`. `ApiBatch` wraps `ODataBatch` with the same settings. + +Requires `EasyAF.Configuration` and `EasyAF.Http`. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Restier.Breakdance/readme.md b/src/CloudNimble.EasyAF.Restier.Breakdance/readme.md new file mode 100644 index 0000000..276a945 --- /dev/null +++ b/src/CloudNimble.EasyAF.Restier.Breakdance/readme.md @@ -0,0 +1,25 @@ +# EasyAF.Restier.Breakdance + +Breakdance test-host helpers for EasyAF Restier APIs. + +## Install + +```bash +dotnet add package EasyAF.Restier.Breakdance +``` + +## Usage + +Reference this package from a test project and use the Breakdance host pattern to boot a Restier API in-process (HTTP, configuration, and EasyAF services) without standing up a full server. + +Requires Breakdance and `EasyAF.Restier`. + +## Documentation + +- Docs: https://easyaf.dev +- Breakdance: https://easyaf.dev/breakdance +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Restier.EF6/readme.md b/src/CloudNimble.EasyAF.Restier.EF6/readme.md new file mode 100644 index 0000000..733b277 --- /dev/null +++ b/src/CloudNimble.EasyAF.Restier.EF6/readme.md @@ -0,0 +1,36 @@ +# EasyAF.Restier.EF6 + +Restier Entity Framework 6 API base class with HTTP context, logging, and SimpleMessageBus publishing. + +## Install + +```bash +dotnet add package EasyAF.Restier.EF6 +``` + +## Usage + +```csharp +public class MyApi : EasyAFEntityFrameworkApi +{ + public MyApi( + IServiceProvider serviceProvider, + IHttpContextAccessor httpContextAccessor, + IMessagePublisher messagePublisher, + ILogger> logger) + : base(serviceProvider, httpContextAccessor, messagePublisher, logger) + { + } +} +``` + +For EF Core, use `EasyAF.Restier.EFCore`. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Restier.EFCore/readme.md b/src/CloudNimble.EasyAF.Restier.EFCore/readme.md new file mode 100644 index 0000000..7179091 --- /dev/null +++ b/src/CloudNimble.EasyAF.Restier.EFCore/readme.md @@ -0,0 +1,36 @@ +# EasyAF.Restier.EFCore + +Restier Entity Framework Core API base class with HTTP context, logging, and SimpleMessageBus publishing. + +## Install + +```bash +dotnet add package EasyAF.Restier.EFCore +``` + +## Usage + +```csharp +public class MyApi : EasyAFEntityFrameworkApi +{ + public MyApi( + IServiceProvider serviceProvider, + IHttpContextAccessor httpContextAccessor, + IMessagePublisher messagePublisher, + ILogger> logger) + : base(serviceProvider, httpContextAccessor, messagePublisher, logger) + { + } +} +``` + +For EF6, use `EasyAF.Restier.EF6`. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Restier/readme.md b/src/CloudNimble.EasyAF.Restier/readme.md new file mode 100644 index 0000000..5c3c9ae --- /dev/null +++ b/src/CloudNimble.EasyAF.Restier/readme.md @@ -0,0 +1,26 @@ +# EasyAF.Restier + +Shared Restier helpers for EasyAF OData APIs (operation logging over entity lifecycle). + +## Install + +```bash +dotnet add package EasyAF.Restier +``` + +## Usage + +```csharp +RestierHelpers.LogOperation(entity, RestierOperationType.Inserting); +``` + +Pair this with `EasyAF.Restier.EF6` or `EasyAF.Restier.EFCore` for `EasyAFEntityFrameworkApi`. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.Tools/readme.md b/src/CloudNimble.EasyAF.Tools/readme.md new file mode 100644 index 0000000..79c4bab --- /dev/null +++ b/src/CloudNimble.EasyAF.Tools/readme.md @@ -0,0 +1,37 @@ +# EasyAF + +.NET global tool (`dotnet easyaf`) for initializing projects, scaffolding EDMX, generating code, and publishing docs. + +## Install + +```bash +dotnet tool install --global EasyAF +``` + +Previews: + +```bash +dotnet tool install --global EasyAF --prerelease +``` + +## Usage + +```bash +dotnet easyaf --help +dotnet easyaf init --help +dotnet easyaf setup --help +dotnet easyaf database generate --context-name MyAppDbContext +dotnet easyaf code generate --context-name MyAppDbContext +dotnet easyaf mintlify --input-path MyApp.xml --output-path docs/ +``` + +The tool command name is `dotnet-easyaf` (invoked as `dotnet easyaf`). + +## Documentation + +- Docs: https://easyaf.dev/quickstart +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. diff --git a/src/CloudNimble.EasyAF.XmlDocumentation/readme.md b/src/CloudNimble.EasyAF.XmlDocumentation/readme.md new file mode 100644 index 0000000..037d7db --- /dev/null +++ b/src/CloudNimble.EasyAF.XmlDocumentation/readme.md @@ -0,0 +1,27 @@ +# EasyAF.XmlDocumentation + +Parses compiler-generated XML documentation files into typed members, summaries, remarks, and examples. + +## Install + +```bash +dotnet add package EasyAF.XmlDocumentation +``` + +## Usage + +```csharp +var docs = new AssemblyXmlDocumentation(XDocument.Load("CloudNimble.EasyAF.Core.xml")); +var type = docs.Types["T:CloudNimble.EasyAF.Core.EasyObservableObject"]; +``` + +Used by the EasyAF docs pipeline (`dotnet easyaf mintlify`) to turn XML comments into Mintlify pages. + +## Documentation + +- Docs: https://easyaf.dev +- Source: https://github.com/CloudNimble/EasyAF + +## License + +MIT © CloudNimble, Inc. From 7af9d3d7a8d73508a1c7179502e8400b59939370 Mon Sep 17 00:00:00 2001 From: Robert McLaws <1657085+robertmclaws@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:30:40 -0400 Subject: [PATCH 7/9] Fix EFCoreToEdmx temp file collision across TFMs. OutputGeneratedEdmxForAnalysis used a second-precision temp path, so parallel net8/net9/net10/net11 test hosts locked the same file. Use a Guid filename and delete it after the dump. --- .../SelfReferencingXmlOutputTest.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/SelfReferencingXmlOutputTest.cs b/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/SelfReferencingXmlOutputTest.cs index e5eab5a..7e966e0 100644 --- a/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/SelfReferencingXmlOutputTest.cs +++ b/src/CloudNimble.EasyAF.Tests.EFCoreToEdmx/SelfReferencingXmlOutputTest.cs @@ -36,10 +36,19 @@ public async Task OutputGeneratedEdmxForAnalysis() Console.WriteLine("=== End EDMX Content ==="); // Also save to a file for easier analysis - var outputPath = Path.Combine(Path.GetTempPath(), $"SelfReferencingTest_{DateTime.Now:yyyyMMdd_HHmmss}.edmx"); - await File.WriteAllTextAsync(outputPath, result.EdmxContent); - - Console.WriteLine($"\nEDMX saved to: {outputPath}"); + var outputPath = Path.Combine(Path.GetTempPath(), $"SelfReferencingTest_{Guid.NewGuid():N}.edmx"); + try + { + await File.WriteAllTextAsync(outputPath, result.EdmxContent); + Console.WriteLine($"\nEDMX saved to: {outputPath}"); + } + finally + { + if (File.Exists(outputPath)) + { + File.Delete(outputPath); + } + } // Verify it contains our Part entity result.EdmxContent.Should().Contain("Part", "EDMX should contain Part entity"); From af76b8d6eff108c96597802d85c2347b325da301 Mon Sep 17 00:00:00 2001 From: Robert McLaws <1657085+robertmclaws@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:32:52 -0400 Subject: [PATCH 8/9] Thanks, Postrges! --- .../CloudNimble.EasyAF.EFCoreToEdmx.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CloudNimble.EasyAF.EFCoreToEdmx/CloudNimble.EasyAF.EFCoreToEdmx.csproj b/src/CloudNimble.EasyAF.EFCoreToEdmx/CloudNimble.EasyAF.EFCoreToEdmx.csproj index d146b45..689d3a9 100644 --- a/src/CloudNimble.EasyAF.EFCoreToEdmx/CloudNimble.EasyAF.EFCoreToEdmx.csproj +++ b/src/CloudNimble.EasyAF.EFCoreToEdmx/CloudNimble.EasyAF.EFCoreToEdmx.csproj @@ -13,6 +13,8 @@ + + From 06e349c7e61f3bad7d1299d8c26665833c97733d Mon Sep 17 00:00:00 2001 From: Robert McLaws <1657085+robertmclaws@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:45:43 -0400 Subject: [PATCH 9/9] Install the .NET 11 SDK in the deploy job. Checkout brings global.json, so dotnet nuget push fails on the runner without 11.0.100-rc.1. --- .github/workflows/build-and-deploy.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 96036bc..5eb99a2 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -184,6 +184,16 @@ jobs: - name: Checkout code uses: actions/checkout@v5 + - name: Install .NET SDK + shell: pwsh + run: | + # Checkout brings global.json, so `dotnet nuget push` needs the 11.0.1xx SDK. + Invoke-WebRequest -Uri "https://dot.net/v1/dotnet-install.ps1" -OutFile "dotnet-install.ps1" + $installDir = "$env:ProgramFiles\dotnet" + ./dotnet-install.ps1 -Channel 11.0.1xx -Quality preview -InstallDir $installDir + echo $installDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + dotnet --list-sdks + - name: Download artifacts uses: actions/download-artifact@v8 with: