diff --git a/ODataCodeGenTools.sln b/ODataCodeGenTools.sln index 0a433b62..dc349366 100644 --- a/ODataCodeGenTools.sln +++ b/ODataCodeGenTools.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29613.14 +# Visual Studio Version 18 +VisualStudioVersion = 18.3.11312.210 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ODataConnectedService", "src\ODataConnectedService\ODataConnectedService.csproj", "{A8BC5B8E-9AB7-4257-B8F1-E7C62169F9B5}" EndProject diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 911da61e..ad8f7554 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -42,6 +42,12 @@ stages: - job: Build steps: + - task: UseDotNet@2 + displayName: 'Use .NET SDK 8.0.x' + inputs: + packageType: 'sdk' + version: '8.0.x' + - task: NuGetToolInstaller@0 inputs: versionSpec: '>=5.2.0' @@ -57,6 +63,7 @@ stages: solution: '$(sln)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' + msbuildArgs: '/p:DeployExtension=false' - task: DotNetCoreCLI@2 displayName: 'Pack Microsoft.OData.Cli package' diff --git a/src/Microsoft.OData.Cli/ODataCliFileHandler.cs b/src/Microsoft.OData.Cli/ODataCliFileHandler.cs index cd9e1719..0ae360b2 100644 --- a/src/Microsoft.OData.Cli/ODataCliFileHandler.cs +++ b/src/Microsoft.OData.Cli/ODataCliFileHandler.cs @@ -96,6 +96,15 @@ public Task EmitContainerPropertyAttributeAsync() return Task.FromResult(true); } + /// + /// True if the native date and time types can be emitted; otherwise, false. + /// + /// A bool indicating whether to emit native date and time types or not + public Task EmitNativeDateTimeTypesAsync() + { + return Task.FromResult(this.project.CheckODataClientVersion()); + } + /// /// Sets the CSDL file as an embedded resource /// diff --git a/src/Microsoft.OData.Cli/ProjectHelper.cs b/src/Microsoft.OData.Cli/ProjectHelper.cs index dc330fd6..29e159c0 100644 --- a/src/Microsoft.OData.Cli/ProjectHelper.cs +++ b/src/Microsoft.OData.Cli/ProjectHelper.cs @@ -143,5 +143,34 @@ internal static string[] GetProjectTargetFrameworks(this Project project) return targetFrameworks; } + + /// + /// Checks if the Microsoft.OData.Client package version in the project is at least 9.0.0. + /// + /// An instance of the loaded . + /// True if the Microsoft.OData.Client version is at least 9.0.0; otherwise, false. + internal static bool CheckODataClientVersion(this Project project) + { + if (project == null) + { + return false; + } + + var version = project.GetItems("PackageReference") + .FirstOrDefault(pr => pr.EvaluatedInclude.Equals("Microsoft.OData.Client", StringComparison.OrdinalIgnoreCase)) + ?.GetMetadataValue("Version"); + + if (string.IsNullOrEmpty(version)) + { + return false; + } + + if (version.Contains('-')) + { + version = version.Substring(0, version.IndexOf("-")); + } + + return Version.TryParse(version, out Version odataClientVersion) && odataClientVersion >= Version.Parse("9.0.0"); + } } } diff --git a/src/Microsoft.OData.CodeGen/CodeGeneration/V4CodeGenDescriptor.cs b/src/Microsoft.OData.CodeGen/CodeGeneration/V4CodeGenDescriptor.cs index bc32ef2c..0ab5506a 100644 --- a/src/Microsoft.OData.CodeGen/CodeGeneration/V4CodeGenDescriptor.cs +++ b/src/Microsoft.OData.CodeGen/CodeGeneration/V4CodeGenDescriptor.cs @@ -196,6 +196,9 @@ private async Task AddGeneratedCodeAsync(string metadata, string outputDirectory await FileHandler.SetFileAsEmbeddedResourceAsync(csdlFileName); t4CodeGenerator.EmitContainerPropertyAttribute = await FileHandler.EmitContainerPropertyAttributeAsync(); + // Determine whether to emit native DateOnly and TimeOnly types + t4CodeGenerator.EmitNativeDateTimeTypes = await FileHandler.EmitNativeDateTimeTypesAsync(); + t4CodeGenerator.MetadataFilePath = metadataFile; t4CodeGenerator.MetadataFileRelativePath = csdlFileName; diff --git a/src/Microsoft.OData.CodeGen/FileHandling/IFileHandler.cs b/src/Microsoft.OData.CodeGen/FileHandling/IFileHandler.cs index 5f313079..8dbe9b19 100644 --- a/src/Microsoft.OData.CodeGen/FileHandling/IFileHandler.cs +++ b/src/Microsoft.OData.CodeGen/FileHandling/IFileHandler.cs @@ -31,7 +31,14 @@ public interface IFileHandler /// /// Emits container property attribute /// - /// + /// >A task that represents the asynchronous operation. true if container property can be emitted; otherwise false Task EmitContainerPropertyAttributeAsync(); + + /// + /// Emits dotnet native date and time types (DateOnly and TimeOnly) to the target environment asynchronously. + /// + /// A task that represents the asynchronous operation. true if the native + /// date and time types can be emitted; otherwise, false. + Task EmitNativeDateTimeTypesAsync(); } } diff --git a/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.cs b/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.cs index 8f70adb8..5de1263c 100644 --- a/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.cs +++ b/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.cs @@ -209,7 +209,8 @@ public virtual async Task TransformTextAsync() ExcludedOperationImports = this.ExcludedOperationImports, ExcludedBoundOperations = this.ExcludedBoundOperations, ExcludedSchemaTypes = this.ExcludedSchemaTypes, - EmitContainerPropertyAttribute = this.EmitContainerPropertyAttribute + EmitContainerPropertyAttribute = this.EmitContainerPropertyAttribute, + EmitNativeDateTimeTypes = this.EmitNativeDateTimeTypes }; } else @@ -250,7 +251,8 @@ public virtual async Task TransformTextAsync() ExcludedOperationImports = this.ExcludedOperationImports, ExcludedBoundOperations = this.ExcludedBoundOperations, ExcludedSchemaTypes = this.ExcludedSchemaTypes, - EmitContainerPropertyAttribute = this.EmitContainerPropertyAttribute + EmitContainerPropertyAttribute = this.EmitContainerPropertyAttribute, + EmitNativeDateTimeTypes = this.EmitNativeDateTimeTypes }; } @@ -568,6 +570,15 @@ public bool EmitContainerPropertyAttribute internal set; } +/// +/// true to emit .NET date/time types (DateOnly and TimeOnly) instead of Microsoft.OData.Edm.Date and Microsoft.OData.Edm.TimeOfDay, false otherwise +/// +public bool EmitNativeDateTimeTypes +{ + get; + internal set; +} + /// /// Generate code targeting a specific .Net Framework language. /// @@ -1333,6 +1344,17 @@ public bool EmitContainerPropertyAttribute set; } + /// + /// true to use native .NET date/time types (DateOnly and TimeOnly) instead of + /// Microsoft.OData.Edm.Date and Microsoft.OData.Edm.TimeOfDay, false otherwise. + /// Set to true when targeting .NET 10.0 or later. + /// + public bool EmitNativeDateTimeTypes + { + get; + set; + } + /// /// true if this EntityContainer need to set the UrlConvention to KeyAsSegment, false otherwise. /// @@ -4298,10 +4320,10 @@ public ODataClientCSharpTemplate(CodeGenerationContext context) internal override string GeometryMultiPolygonTypeName { get { return "global::Microsoft.Spatial.GeometryMultiPolygon"; } } internal override string GeometryMultiLineStringTypeName { get { return "global::Microsoft.Spatial.GeometryMultiLineString"; } } internal override string GeometryMultiPointTypeName { get { return "global::Microsoft.Spatial.GeometryMultiPoint"; } } - internal override string DateTypeName { get { return "global::Microsoft.OData.Edm.Date"; } } + internal override string DateTypeName { get { return this.context.EmitNativeDateTimeTypes ? "global::System.DateOnly" : "global::Microsoft.OData.Edm.Date"; } } internal override string DateTimeOffsetTypeName { get { return "global::System.DateTimeOffset"; } } internal override string DurationTypeName { get { return "global::System.TimeSpan"; } } - internal override string TimeOfDayTypeName { get { return "global::Microsoft.OData.Edm.TimeOfDay"; } } + internal override string TimeOfDayTypeName { get { return this.context.EmitNativeDateTimeTypes ? "global::System.TimeOnly" : "global::Microsoft.OData.Edm.TimeOfDay"; } } internal override string XmlConvertClassName { get { return "global::System.Xml.XmlConvert"; } } internal override string EnumTypeName { get { return "global::System.Enum"; } } internal override string DictionaryInterfaceName { get { return "global::System.Collections.Generic.IDictionary<{0}, {1}>"; } } @@ -6441,10 +6463,10 @@ public ODataClientVBTemplate(CodeGenerationContext context) internal override string GeometryMultiPolygonTypeName { get { return "Global.Microsoft.Spatial.GeometryMultiPolygon"; } } internal override string GeometryMultiLineStringTypeName { get { return "Global.Microsoft.Spatial.GeometryMultiLineString"; } } internal override string GeometryMultiPointTypeName { get { return "Global.Microsoft.Spatial.GeometryMultiPoint"; } } - internal override string DateTypeName { get { return "Global.Microsoft.OData.Edm.Date"; } } + internal override string DateTypeName { get { return this.context.EmitNativeDateTimeTypes ? "Global.System.DateOnly" : "Global.Microsoft.OData.Edm.Date"; } } internal override string DateTimeOffsetTypeName { get { return "Global.System.DateTimeOffset"; } } internal override string DurationTypeName { get { return "Global.System.TimeSpan"; } } - internal override string TimeOfDayTypeName { get { return "Global.Microsoft.OData.Edm.TimeOfDay"; } } + internal override string TimeOfDayTypeName { get { return this.context.EmitNativeDateTimeTypes ? "Global.System.TimeOnly" : "Global.Microsoft.OData.Edm.TimeOfDay"; } } internal override string XmlConvertClassName { get { return "Global.System.Xml.XmlConvert"; } } internal override string EnumTypeName { get { return "Global.System.Enum"; } } internal override string DictionaryInterfaceName { get { return "Global.System.Collections.Generic.IDictionary(Of {0}, {1})"; } } diff --git a/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.ttinclude b/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.ttinclude index f50826fe..309b1743 100644 --- a/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.ttinclude +++ b/src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.ttinclude @@ -66,7 +66,8 @@ public virtual async Task TransformTextAsync() ExcludedOperationImports = this.ExcludedOperationImports, ExcludedBoundOperations = this.ExcludedBoundOperations, ExcludedSchemaTypes = this.ExcludedSchemaTypes, - EmitContainerPropertyAttribute = this.EmitContainerPropertyAttribute + EmitContainerPropertyAttribute = this.EmitContainerPropertyAttribute, + EmitNativeDateTimeTypes = this.EmitNativeDateTimeTypes }; } else @@ -107,7 +108,8 @@ public virtual async Task TransformTextAsync() ExcludedOperationImports = this.ExcludedOperationImports, ExcludedBoundOperations = this.ExcludedBoundOperations, ExcludedSchemaTypes = this.ExcludedSchemaTypes, - EmitContainerPropertyAttribute = this.EmitContainerPropertyAttribute + EmitContainerPropertyAttribute = this.EmitContainerPropertyAttribute, + EmitNativeDateTimeTypes = this.EmitNativeDateTimeTypes }; } @@ -425,6 +427,15 @@ public bool EmitContainerPropertyAttribute internal set; } +/// +/// true to emit .NET date/time types (DateOnly and TimeOnly) instead of Microsoft.OData.Edm.Date and Microsoft.OData.Edm.TimeOfDay, false otherwise +/// +public bool EmitNativeDateTimeTypes +{ + get; + internal set; +} + /// /// Generate code targeting a specific .Net Framework language. /// @@ -1190,6 +1201,17 @@ public class CodeGenerationContext set; } + /// + /// true to use native .NET date/time types (DateOnly and TimeOnly) instead of + /// Microsoft.OData.Edm.Date and Microsoft.OData.Edm.TimeOfDay, false otherwise. + /// Set to true when targeting .NET 10.0 or later. + /// + public bool EmitNativeDateTimeTypes + { + get; + set; + } + /// /// true if this EntityContainer need to set the UrlConvention to KeyAsSegment, false otherwise. /// diff --git a/src/ODataConnectedService.Shared/ConnectedServiceFileHandler.cs b/src/ODataConnectedService.Shared/ConnectedServiceFileHandler.cs index d81b1756..26f7ab16 100644 --- a/src/ODataConnectedService.Shared/ConnectedServiceFileHandler.cs +++ b/src/ODataConnectedService.Shared/ConnectedServiceFileHandler.cs @@ -9,10 +9,10 @@ using System.Threading.Tasks; using EnvDTE; using Microsoft.OData.CodeGen.FileHandling; +using Microsoft.OData.ConnectedService.Threading; using Microsoft.VisualStudio.ConnectedServices; using Microsoft.VisualStudio.Shell; using VSLangProj; -using Microsoft.OData.ConnectedService.Threading; using Task = System.Threading.Tasks.Task; namespace Microsoft.OData.ConnectedService @@ -25,6 +25,10 @@ public class ConnectedServiceFileHandler : IFileHandler private ConnectedServiceHandlerContext Context; private readonly IThreadHelper threadHelper; + // Cache the OData Client version to avoid multiple project references enumeration + private Version odataClientVersion = null; + private bool isOdataClientVersionCached = false; + public Project Project { get; private set; } /// @@ -70,7 +74,7 @@ await this.threadHelper.RunInUiThreadAsync(() => } #pragma warning restore VSTHRD010 // This invokes the code in the required main thread. return false; - }); + }).ConfigureAwait(false); } /// @@ -79,27 +83,55 @@ await this.threadHelper.RunInUiThreadAsync(() => /// /// A value of either true or false public Task EmitContainerPropertyAttributeAsync() - => threadHelper.RunInUiThreadAsync(() => + => this.CheckODataClientVersionAsync(version => version > Version.Parse("7.6.4.0")); + + /// + /// Determines asynchronously whether native date and time types are supported by the connected OData service. + /// + /// A task that represents the asynchronous operation. True if native date and time types are supported; otherwise, false. + public Task EmitNativeDateTimeTypesAsync() + => this.CheckODataClientVersionAsync(version => version >= Version.Parse("9.0.0") || version >= Version.Parse("9.0.0.0")); + + /// + /// Checks if the Microsoft.OData.Client reference meets a version condition. + /// + /// A predicate to evaluate against the OData Client version. + /// True if the reference exists and meets the version condition; otherwise false. + private Task CheckODataClientVersionAsync(Func versionPredicate) + { + return this.threadHelper.RunInUiThreadAsync(() => + { + if (this.isOdataClientVersionCached && this.odataClientVersion != null) + { + return versionPredicate(this.odataClientVersion); + } - { #pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread - if (this.Project.Object is VSProject vsProject) - { - foreach (Reference reference in vsProject.References) - { - if (reference.SourceProject == null) - { - // Assembly reference (For project reference, SourceProject != null) - if (reference.Name.Equals("Microsoft.OData.Client", StringComparison.Ordinal)) - { - return Version.Parse(reference.Version) > Version.Parse("7.6.4.0"); - } - } - } - } + if (this.Project.Object is VSProject vsProject) + { + foreach (Reference reference in vsProject.References) + { + if (reference.SourceProject == null && + reference.Name.Equals("Microsoft.OData.Client", StringComparison.Ordinal)) + { + var currentVersion = reference.Version; + if (currentVersion.Contains("-")) + { + currentVersion = currentVersion.Substring(0, currentVersion.IndexOf('-')); + } + + this.odataClientVersion = Version.Parse(currentVersion); + this.isOdataClientVersionCached = true; + return versionPredicate(this.odataClientVersion); + } + } + } #pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread - return false; - }); + this.odataClientVersion = null; + this.isOdataClientVersionCached = true; + return false; + }); + } } -} \ No newline at end of file +} diff --git a/src/ODataConnectedService.Shared/ConnectedServicePackageInstaller.cs b/src/ODataConnectedService.Shared/ConnectedServicePackageInstaller.cs index 5cac7f2d..a8b46563 100644 --- a/src/ODataConnectedService.Shared/ConnectedServicePackageInstaller.cs +++ b/src/ODataConnectedService.Shared/ConnectedServicePackageInstaller.cs @@ -70,7 +70,7 @@ public async Task CheckAndInstallNuGetPackageAsync(string packageSource, string { if (!PackageInstallerServices.IsPackageInstalled(this.Project, packageName)) { - PackageInstaller.InstallPackage(packageSource, this.Project, packageName, (string)null, false); ; + PackageInstaller.InstallPackage(packageSource, this.Project, packageName, (string)null, false); await (this.MessageLogger?.WriteMessageAsync(LogMessageCategory.Information, $"Nuget Package \"{packageName}\" for OData client was added.")).ConfigureAwait(false); } diff --git a/src/ODataConnectedService_VS2022Plus/source.extension.vsixmanifest b/src/ODataConnectedService_VS2022Plus/source.extension.vsixmanifest index 4d5e0bf5..c17762a6 100644 --- a/src/ODataConnectedService_VS2022Plus/source.extension.vsixmanifest +++ b/src/ODataConnectedService_VS2022Plus/source.extension.vsixmanifest @@ -1,7 +1,7 @@ - + OData Connected Service 2022+ OData Connected Service for V1-V4 https://github.com/odata/ODataConnectedService @@ -10,33 +10,33 @@ OData Connected Service - + amd64 - + amd64 - + amd64 - + arm64 - + arm64 - + arm64 - + - + - + \ No newline at end of file diff --git a/test/Microsoft.OData.Cli.Tests/CodeGeneration/Artifacts/SampleServiceV4WithDateOnlyAndTimeOnly.xml b/test/Microsoft.OData.Cli.Tests/CodeGeneration/Artifacts/SampleServiceV4WithDateOnlyAndTimeOnly.xml new file mode 100644 index 00000000..7f59d510 --- /dev/null +++ b/test/Microsoft.OData.Cli.Tests/CodeGeneration/Artifacts/SampleServiceV4WithDateOnlyAndTimeOnly.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.OData.Cli.Tests/CodeGeneration/ODataCliCodeGenerationTests.cs b/test/Microsoft.OData.Cli.Tests/CodeGeneration/ODataCliCodeGenerationTests.cs index 702a4b37..852626c9 100644 --- a/test/Microsoft.OData.Cli.Tests/CodeGeneration/ODataCliCodeGenerationTests.cs +++ b/test/Microsoft.OData.Cli.Tests/CodeGeneration/ODataCliCodeGenerationTests.cs @@ -8,6 +8,8 @@ using System.CommandLine; using System.CommandLine.Parsing; using System.Text.RegularExpressions; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Locator; using Microsoft.OData.CodeGen.Common; namespace Microsoft.OData.Cli.Tests.CodeGeneration @@ -16,6 +18,7 @@ public class ODataCliCodeGenerationTests : IDisposable { private readonly GenerateCommand generateCommand; private readonly string metadataUri = Path.Combine(Environment.CurrentDirectory, "CodeGeneration\\Artifacts\\SampleServiceV4.xml"); + private readonly string metadataUriWithDateOnlyAndTimeOnly = Path.Combine(Environment.CurrentDirectory, "CodeGeneration\\Artifacts\\SampleServiceV4WithDateOnlyAndTimeOnly.xml"); private readonly string lowerCamelCaseMetadataUri = Path.Combine(Environment.CurrentDirectory, "CodeGeneration\\Artifacts\\SampleServiceV4LowerCamelCase.xml"); private readonly string outputDir; @@ -23,6 +26,9 @@ public ODataCliCodeGenerationTests() { this.generateCommand = new GenerateCommand(); this.outputDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + // Ensure MSBuild is registered for Project API usage + EnsureMSBuildLoadedIfNot(); } [Fact] @@ -489,10 +495,49 @@ public void TestCodeGeneratedForServiceNameOption(string commandLine) Regex.Replace(inputCsdlContent, @"\s+", "")); } + [Fact] + public void TestCodeGeneration_WithODataClient9_UsesNativeDateTimeTypes() + { + this.CreateTestProjectInOutputDir("net8.0", "9.0.0-preview.3"); + + var parseResult = this.generateCommand.Parse($"--metadata-uri {this.metadataUriWithDateOnlyAndTimeOnly} --outputdir {this.outputDir}"); + parseResult.Invoke(); + + var referenceProxyFile = Assert.Single(Directory.GetFiles(outputDir, $"{Constants.DefaultReferenceFileName}.cs")); + var generatedCode = File.ReadAllText(referenceProxyFile); + + Assert.NotNull(generatedCode); + Assert.NotEmpty(generatedCode); + + Assert.Contains("public virtual global::System.DateOnly OrderDate", generatedCode); + Assert.Contains("public virtual global::System.TimeOnly OrderTime", generatedCode); + } + + [Fact] + public void TestCodeGeneration_WithODataClientVersionLessThan9_UsesNativeDateTimeTypes() + { + this.CreateTestProjectInOutputDir("net8.0", "8.4.3"); + + var parseResult = this.generateCommand.Parse($"--metadata-uri {this.metadataUriWithDateOnlyAndTimeOnly} --outputdir {this.outputDir}"); + parseResult.Invoke(); + + var referenceProxyFile = Assert.Single(Directory.GetFiles(outputDir, $"{Constants.DefaultReferenceFileName}.cs")); + var generatedCode = File.ReadAllText(referenceProxyFile); + + Assert.NotNull(generatedCode); + Assert.NotEmpty(generatedCode); + + Assert.Contains("public virtual global::Microsoft.OData.Edm.Date OrderDate", generatedCode); + Assert.Contains("public virtual global::Microsoft.OData.Edm.TimeOfDay OrderTime", generatedCode); + } + public void Dispose() { try { + // Unload MSBuild projects before deleting directory + ProjectCollection.GlobalProjectCollection.UnloadAllProjects(); + // Delete the temp directory if possible if (Directory.Exists(outputDir)) { @@ -504,5 +549,54 @@ public void Dispose() // Ignore - Temporary files are eventually clean up } } + + private void CreateTestProjectInOutputDir(string targetFramework, string odataClientVersion) + { + if (!Directory.Exists(this.outputDir)) + { + Directory.CreateDirectory(this.outputDir); + } + + var projectPath = Path.Combine(this.outputDir, "TestProject.csproj"); + + var projectContent = $@" + + + Exe + {targetFramework} + enable + enable + + + + + + + + + + + +"; + + File.WriteAllText(projectPath, projectContent); + } + + private static void EnsureMSBuildLoadedIfNot() + { + if (!MSBuildLocator.IsRegistered) + { + try + { + MSBuildLocator.RegisterDefaults(); + } + catch (InvalidOperationException) + { + // MSBuild assemblies were already loaded before registration + // This can happen if another test class already loaded MSBuild types + // Safe to ignore since MSBuild is already available + } + } + } } } diff --git a/test/Microsoft.OData.Cli.Tests/FileHandling/ODataCliFileHandlerTests.cs b/test/Microsoft.OData.Cli.Tests/FileHandling/ODataCliFileHandlerTests.cs new file mode 100644 index 00000000..94c4bdfc --- /dev/null +++ b/test/Microsoft.OData.Cli.Tests/FileHandling/ODataCliFileHandlerTests.cs @@ -0,0 +1,194 @@ +//----------------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//----------------------------------------------------------------------------------- + +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Locator; +using Microsoft.OData.CodeGen.Logging; +using Moq; + +namespace Microsoft.OData.Cli.Tests.FileHandling +{ + public class ODataCliFileHandlerTests + { + public ODataCliFileHandlerTests() + { + EnsureMSBuildLoadedIfNot(); + } + + [Fact] + public async Task EmitNativeDateTimeTypesAsync_ShouldReturnTrue_WhenODataClientVersionIsGreaterThanOrEqualTo9_0_0() + { + // Arrange + var project = CreateProjectWithODataClientVersion("9.0.0"); + var fileHandler = CreateFileHandler(project); + + // Act + var result = await fileHandler.EmitNativeDateTimeTypesAsync(); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task EmitNativeDateTimeTypesAsync_ShouldReturnTrue_WhenODataClientVersionIs9_1_0() + { + // Arrange + var project = CreateProjectWithODataClientVersion("9.1.0"); + var fileHandler = CreateFileHandler(project); + + // Act + var result = await fileHandler.EmitNativeDateTimeTypesAsync(); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task EmitNativeDateTimeTypesAsync_ShouldReturnTrue_WhenODataClientVersionIsPrereleaseAsync() + { + // Arrange + var project = CreateProjectWithODataClientVersion("9.0.0-preview.3"); + var fileHandler = CreateFileHandler(project); + + // Act + var result = await fileHandler.EmitNativeDateTimeTypesAsync(); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task EmitNativeDateTimeTypesAsync_ShouldReturnFalse_WhenODataClientVersionIsLessThan9_0_0() + { + // Arrange + var project = CreateProjectWithODataClientVersion("8.0.0"); + var fileHandler = CreateFileHandler(project); + + // Act + var result = await fileHandler.EmitNativeDateTimeTypesAsync(); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task EmitNativeDateTimeTypesAsync_ShouldReturnFalse_WhenODataClientVersionIs7_6_4() + { + // Arrange + var project = CreateProjectWithODataClientVersion("7.6.4"); + var fileHandler = CreateFileHandler(project); + + // Act + var result = await fileHandler.EmitNativeDateTimeTypesAsync(); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task EmitNativeDateTimeTypesAsync_ShouldReturnFalse_WhenODataClientReferenceNotFound() + { + // Arrange + var project = CreateProjectWithoutODataClient(); + var fileHandler = CreateFileHandler(project); + + // Act + var result = await fileHandler.EmitNativeDateTimeTypesAsync(); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task EmitNativeDateTimeTypesAsync_ShouldReturnFalse_WhenProjectIsNull() + { + // Arrange + var fileHandler = CreateFileHandler(null); + + // Act + var result = await fileHandler.EmitNativeDateTimeTypesAsync(); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task EmitNativeDateTimeTypesAsync_ShouldReturnFalse_WhenVersionCannotBeParsed() + { + // Arrange + var project = CreateProjectWithODataClientVersion("invalid-version"); + var fileHandler = CreateFileHandler(project); + + // Act + var result = await fileHandler.EmitNativeDateTimeTypesAsync(); + + // Assert + Assert.False(result); + } + + private static Project CreateProjectWithODataClientVersion(string version) + { + // Create a .csproj in memory + var pre = ProjectRootElement.Create(); + + var pg = pre.AddPropertyGroup(); + pg.AddProperty("TargetFramework", "net8.0"); + + var ig = pre.AddItemGroup(); + var pr = ig.AddItem("PackageReference", "Microsoft.OData.Client"); + pr.AddMetadata("Version", version, expressAsAttribute: true); + + // Create an evaluated Project from the XML + var project = new Project(pre); + project.ReevaluateIfNecessary(); + + return project; + } + + private static Project CreateProjectWithoutODataClient() + { + // Create a .csproj in memory + var pre = ProjectRootElement.Create(); + + var pg = pre.AddPropertyGroup(); + pg.AddProperty("TargetFramework", "net8.0"); + + var ig = pre.AddItemGroup(); + var pr = ig.AddItem("PackageReference", "Newtonsoft.Json"); + pr.AddMetadata("Version", "13.0.1", expressAsAttribute: true); + + var project = new Project(pre); + project.ReevaluateIfNecessary(); + + return project; + } + + private static ODataCliFileHandler CreateFileHandler(Project project) + { + var loggerMock = new Mock(); + return new ODataCliFileHandler(loggerMock.Object, project); + } + + private static void EnsureMSBuildLoadedIfNot() + { + if (!MSBuildLocator.IsRegistered) + { + try + { + MSBuildLocator.RegisterDefaults(); + } + catch (InvalidOperationException) + { + // MSBuild assemblies were already loaded before registration + // This can happen if another test class already loaded MSBuild types + // Safe to ignore since MSBuild is already available + } + } + } + } +} diff --git a/test/Microsoft.OData.Cli.Tests/Microsoft.OData.Cli.Tests.csproj b/test/Microsoft.OData.Cli.Tests/Microsoft.OData.Cli.Tests.csproj index 528c0eec..171535b6 100644 --- a/test/Microsoft.OData.Cli.Tests/Microsoft.OData.Cli.Tests.csproj +++ b/test/Microsoft.OData.Cli.Tests/Microsoft.OData.Cli.Tests.csproj @@ -60,6 +60,7 @@ + @@ -73,6 +74,9 @@ + + Always + Always diff --git a/test/ODataConnectedService.Tests/FileHandling/ConnectedServiceFileHandlerTests.cs b/test/ODataConnectedService.Tests/FileHandling/ConnectedServiceFileHandlerTests.cs new file mode 100644 index 00000000..84500830 --- /dev/null +++ b/test/ODataConnectedService.Tests/FileHandling/ConnectedServiceFileHandlerTests.cs @@ -0,0 +1,176 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//---------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Threading.Tasks; +using EnvDTE; +using Microsoft.OData.CodeGen.Models; +using Microsoft.OData.ConnectedService; +using Microsoft.OData.ConnectedService.Tests.TestHelpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using ODataConnectedService.Tests.TestHelpers; +using VSLangProj; + +namespace ODataConnectedService.Tests.FileHandling +{ + [TestClass] + public class ConnectedServiceFileHandlerTests + { + [TestMethod] + public async Task EmitNativeDateTimeTypesAsync_ShouldReturnTrue_WhenODataClientVersionIsGreaterThanOrEqualTo9_0_0Async() + { + // Arrange + var project = CreateProjectWithODataClientVersion("9.0.0"); + var fileHandler = CreateFileHandler(project); + + // Act + var result = await fileHandler.EmitNativeDateTimeTypesAsync().ConfigureAwait(false); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + public async Task EmitNativeDateTimeTypesAsync_ShouldReturnTrue_WhenODataClientVersionIsPrereleaseAsync() + { + // Arrange + var project = CreateProjectWithODataClientVersion("9.0.0-preview.3"); + var fileHandler = CreateFileHandler(project); + + // Act + var result = await fileHandler.EmitNativeDateTimeTypesAsync().ConfigureAwait(false); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + public async Task EmitNativeDateTimeTypesAsync_ShouldReturnFalse_WhenODataClientVersionIsLessThan9_0_0_Async() + { + // Arrange + var project = CreateProjectWithODataClientVersion("8.0.0"); + var fileHandler = CreateFileHandler(project); + + // Act + var result = await fileHandler.EmitNativeDateTimeTypesAsync().ConfigureAwait(false); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + public async Task EmitNativeDateTimeTypesAsync_ShouldReturnFalse_WhenODataClientReferenceNotFoundAsync() + { + // Arrange + var project = CreateProjectWithoutODataClient(); + var fileHandler = CreateFileHandler(project); + + // Act + var result = await fileHandler.EmitNativeDateTimeTypesAsync().ConfigureAwait(false); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + public async Task CheckODataClientVersionAsync_ShouldCacheVersion_AndReuseOnSubsequentCallsAsync() + { + // Arrange + var referenceMock = new Mock(); + referenceMock.SetupGet(r => r.Name).Returns("Microsoft.OData.Client"); + referenceMock.SetupGet(r => r.Version).Returns("9.0.0.0"); + referenceMock.SetupGet(r => r.SourceProject).Returns((Project)null); + + var referencesMock = new Mock(); + var callCount = 0; + referencesMock.Setup(r => r.GetEnumerator()) + .Returns(() => + { + callCount++; + return new List { referenceMock.Object }.GetEnumerator(); + }); + + var vsProjectMock = new Mock(); + vsProjectMock.SetupGet(vsp => vsp.References).Returns(referencesMock.Object); + + var projectMock = new Mock(); + projectMock.SetupGet(p => p.Object).Returns(vsProjectMock.Object); + + var fileHandler = CreateFileHandler(projectMock.Object); + + // Act + var result1 = await fileHandler.EmitNativeDateTimeTypesAsync().ConfigureAwait(false); + var result2 = await fileHandler.EmitContainerPropertyAttributeAsync().ConfigureAwait(false); + + // Assert + Assert.IsTrue(result1); + Assert.IsTrue(result2); + Assert.AreEqual(1, callCount, "References should only be enumerated once due to caching"); + } + + private static Project CreateProjectWithODataClientVersion(string version) + { + var referenceMock = new Mock(); + referenceMock.SetupGet(r => r.Name).Returns("Microsoft.OData.Client"); + referenceMock.SetupGet(r => r.Version).Returns(version); + referenceMock.SetupGet(r => r.SourceProject).Returns((Project)null); + + var referencesMock = new Mock(); + referencesMock.Setup(r => r.GetEnumerator()) + .Returns(new List { referenceMock.Object }.GetEnumerator()); + + var vsProjectMock = new Mock(); + vsProjectMock.SetupGet(vsp => vsp.References).Returns(referencesMock.Object); + + var projectMock = new Mock(); + projectMock.SetupGet(p => p.Object).Returns(vsProjectMock.Object); + + return projectMock.Object; + } + + private static Project CreateProjectWithoutODataClient() + { + var referenceMock = new Mock(); + referenceMock.SetupGet(r => r.Name).Returns("System.Core"); + referenceMock.SetupGet(r => r.Version).Returns("4.0.0.0"); + referenceMock.SetupGet(r => r.SourceProject).Returns((Project)null); + + var referencesMock = new Mock(); + referencesMock.Setup(r => r.GetEnumerator()) + .Returns(new List { referenceMock.Object }.GetEnumerator()); + + var vsProjectMock = new Mock(); + vsProjectMock.SetupGet(vsp => vsp.References).Returns(referencesMock.Object); + + var projectMock = new Mock(); + projectMock.SetupGet(p => p.Object).Returns(vsProjectMock.Object); + + return projectMock.Object; + } + + private static ConnectedServiceFileHandler CreateFileHandler(Project project) + { + var serviceConfig = new ServiceConfigurationV4 { ServiceName = "TestService" }; + var serviceInstance = new ODataConnectedServiceInstance + { + ServiceConfig = serviceConfig, + Name = "TestService" + }; + + var handlerHelper = new TestConnectedServiceHandlerHelper + { + ServicesRootFolder = "ConnectedServices" + }; + + var context = new TestConnectedServiceHandlerContext(serviceInstance, handlerHelper); + var threadHelper = new TestThreadHelper(); + + return new ConnectedServiceFileHandler(context, project, threadHelper); + } + } +} diff --git a/test/ODataConnectedService.Tests/ODataConnectedService.Tests.csproj b/test/ODataConnectedService.Tests/ODataConnectedService.Tests.csproj index 184faf88..1fd79c09 100644 --- a/test/ODataConnectedService.Tests/ODataConnectedService.Tests.csproj +++ b/test/ODataConnectedService.Tests/ODataConnectedService.Tests.csproj @@ -70,6 +70,7 @@ + diff --git a/test/ODataConnectedService.Tests/Templates/ODataT4CodeGeneratorTests.cs b/test/ODataConnectedService.Tests/Templates/ODataT4CodeGeneratorTests.cs index fa7a745a..52e85214 100644 --- a/test/ODataConnectedService.Tests/Templates/ODataT4CodeGeneratorTests.cs +++ b/test/ODataConnectedService.Tests/Templates/ODataT4CodeGeneratorTests.cs @@ -978,5 +978,107 @@ public void GenerateDynamicPropertyContainerWithInheritance() Assert.IsTrue(normalizedGeneratedCode.IndexOf(otherContainerPropertyAttributeSnippet, StringComparison.Ordinal) == -1); } } + + [TestMethod] + public void SetEmitNativeDateTimeTypes_True_GenerateNativeDateTimeTypes() + { + var edmx = @" + + + + + + + + + + + + + + + + + + +"; + + var t4CodeGenerator = new ODataT4CodeGenerator + { + Edmx = edmx, + GetReferencedModelReaderFunc = null, + NamespacePrefix = null, + EnableNamingAlias = false, + IgnoreUnexpectedElementsAndAttributes = false, + GenerateMultipleFiles = false, + ExcludedSchemaTypes = null, + EmitNativeDateTimeTypes = true + }; + + // CSharp + t4CodeGenerator.TargetLanguage = ODataT4CodeGenerator.LanguageOption.CSharp; + var generatedCode = t4CodeGenerator.TransformText(); + + Assert.IsTrue(generatedCode.IndexOf("public virtual global::System.DateOnly OrderDate", StringComparison.Ordinal) > 0); + Assert.IsTrue(generatedCode.IndexOf("public virtual global::System.TimeOnly OrderTime", StringComparison.Ordinal) > 0); + + // VB + t4CodeGenerator.TargetLanguage = ODataT4CodeGenerator.LanguageOption.VB; + generatedCode = t4CodeGenerator.TransformText(); + + Assert.IsTrue(generatedCode.IndexOf("Public Overridable Property OrderDate() As Global.System.DateOnly", StringComparison.Ordinal) > 0); + Assert.IsTrue(generatedCode.IndexOf("Public Overridable Property OrderTime() As Global.System.TimeOnly", StringComparison.Ordinal) > 0); + } + + [TestMethod] + public void SetEmitNativeDateTimeTypes_False_GenerateODataEdmTypes() + { + var edmx = @" + + + + + + + + + + + + + + + + + + +"; + + var t4CodeGenerator = new ODataT4CodeGenerator + { + Edmx = edmx, + GetReferencedModelReaderFunc = null, + NamespacePrefix = null, + EnableNamingAlias = false, + IgnoreUnexpectedElementsAndAttributes = false, + GenerateMultipleFiles = false, + ExcludedSchemaTypes = null, + EmitNativeDateTimeTypes = false + }; + + // CSharp + t4CodeGenerator.TargetLanguage = ODataT4CodeGenerator.LanguageOption.CSharp; + var generatedCode = t4CodeGenerator.TransformText(); + + Assert.IsTrue(generatedCode.IndexOf("public virtual global::Microsoft.OData.Edm.Date OrderDate", StringComparison.Ordinal) > 0); + Assert.IsTrue(generatedCode.IndexOf("public virtual global::Microsoft.OData.Edm.TimeOfDay OrderTime", StringComparison.Ordinal) > 0); + + // VB + t4CodeGenerator.TargetLanguage = ODataT4CodeGenerator.LanguageOption.VB; + generatedCode = t4CodeGenerator.TransformText(); + + Assert.IsTrue(generatedCode.IndexOf("Public Overridable Property OrderDate() As Global.Microsoft.OData.Edm.Date", StringComparison.Ordinal) > 0); + Assert.IsTrue(generatedCode.IndexOf("Public Overridable Property OrderTime() As Global.Microsoft.OData.Edm.TimeOfDay", StringComparison.Ordinal) > 0); + } } }