From 6036cf71013e2bbcc6485adbb456df9c1556893b Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Wed, 4 Sep 2024 14:42:39 +0300 Subject: [PATCH] fix: resolve trimming warnings in project --- ...raph-sdk-dotnet-core-branch-protection.yml | 1 + .github/workflows/sonarcloud.yml | 2 +- .github/workflows/validatePullRequest.yml | 19 +++- .../Microsoft.Graph.Core.csproj | 10 +- .../Tasks/LargeFileUploadTask.cs | 60 ++++++---- .../Tasks/PageIterator.cs | 11 +- .../MockUploadSessionWithoutInterface.cs | 78 +++++++++++++ .../Tasks/LargeFileUploadTaskTests.cs | 107 +++++++++++++++--- ...soft.Graph.DotnetCore.Core.Trimming.csproj | 17 +++ .../Program.cs | 9 ++ .../global.json | 6 + 11 files changed, 274 insertions(+), 46 deletions(-) create mode 100644 tests/Microsoft.Graph.DotnetCore.Core.Test/Mocks/MockUploadSessionWithoutInterface.cs create mode 100644 tests/Microsoft.Graph.DotnetCore.Core.Trimming/Microsoft.Graph.DotnetCore.Core.Trimming.csproj create mode 100644 tests/Microsoft.Graph.DotnetCore.Core.Trimming/Program.cs create mode 100644 tests/Microsoft.Graph.DotnetCore.Core.Trimming/global.json diff --git a/.github/policies/msgraph-sdk-dotnet-core-branch-protection.yml b/.github/policies/msgraph-sdk-dotnet-core-branch-protection.yml index f25d119ea..c7f0b13d4 100644 --- a/.github/policies/msgraph-sdk-dotnet-core-branch-protection.yml +++ b/.github/policies/msgraph-sdk-dotnet-core-branch-protection.yml @@ -36,6 +36,7 @@ configuration: # Required status checks to pass before merging. Values can be any string, but if the value does not correspond to any existing status check, the status check will be stuck on pending for status since nothing exists to push an actual status requiredStatusChecks: - Build and Test # Contains CodeQL + - Validate Project for Trimming - license/cla # Require branches to be up to date before merging. boolean requiresStrictStatusChecks: true diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 2dc402ba6..0d76e741a 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -70,7 +70,7 @@ jobs: CoverletOutputFormat: "opencover" # https://github.com/microsoft/vstest/issues/4014#issuecomment-1307913682 shell: pwsh run: | - ./.sonar/scanner/dotnet-sonarscanner begin /k:"microsoftgraph_msgraph-sdk-dotnet-core" /o:"microsoftgraph2" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="tests/Microsoft.Graph.DotnetCore.Core.Test/coverage.opencover.xml" + ./.sonar/scanner/dotnet-sonarscanner begin /k:"microsoftgraph_msgraph-sdk-dotnet-core" /o:"microsoftgraph2" /d:sonar.scanner.scanAll=false /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="tests/Microsoft.Graph.DotnetCore.Core.Test/coverage.opencover.xml" dotnet workload restore dotnet build dotnet test Microsoft.Graph.Core.sln --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover --framework net6.0 diff --git a/.github/workflows/validatePullRequest.yml b/.github/workflows/validatePullRequest.yml index cf139692d..7311379a3 100644 --- a/.github/workflows/validatePullRequest.yml +++ b/.github/workflows/validatePullRequest.yml @@ -40,12 +40,12 @@ jobs: - name: Install needed dotnet workloads run: dotnet workload install android macos ios maccatalyst + + - name: Restore nuget dependencies + run: dotnet restore ${{ env.solutionName }} - name: Lint the code run: dotnet format --verify-no-changes - - - name: Restore nuget dependencies - run: dotnet restore ${{ env.solutionName }} - name: Build run: dotnet build ${{ env.solutionName }} -c Debug /p:UseSharedCompilation=false,IncludeMauiTargets=true @@ -56,4 +56,17 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 + validate-trimming: + name: Validate Project for Trimming + runs-on: windows-latest + steps: + - uses: actions/checkout@v4.1.7 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.x + - name: Validate Trimming warnings + run: dotnet publish -c Release -r win-x64 /p:TreatWarningsAsErrors=true /warnaserror -f net8.0 + working-directory: ./tests/Microsoft.Graph.DotnetCore.Core.Trimming \ No newline at end of file diff --git a/src/Microsoft.Graph.Core/Microsoft.Graph.Core.csproj b/src/Microsoft.Graph.Core/Microsoft.Graph.Core.csproj index 93bcd32fa..bdd4977fd 100644 --- a/src/Microsoft.Graph.Core/Microsoft.Graph.Core.csproj +++ b/src/Microsoft.Graph.Core/Microsoft.Graph.Core.csproj @@ -62,16 +62,16 @@ - - + + - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Microsoft.Graph.Core/Tasks/LargeFileUploadTask.cs b/src/Microsoft.Graph.Core/Tasks/LargeFileUploadTask.cs index e51778afc..3f5bbba5d 100644 --- a/src/Microsoft.Graph.Core/Tasks/LargeFileUploadTask.cs +++ b/src/Microsoft.Graph.Core/Tasks/LargeFileUploadTask.cs @@ -6,14 +6,17 @@ namespace Microsoft.Graph { using System; using System.Collections.Generic; + using System.ComponentModel; using System.IO; using System.Net.Http; + using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Graph.Core.Models; using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Authentication; using Microsoft.Kiota.Abstractions.Serialization; + using Microsoft.Kiota.Serialization.Json; /// /// Task to help with resumable large file uploads. @@ -40,7 +43,23 @@ private IUploadSession Session /// Max size(in bytes) of each slice to be uploaded. Defaults to 5MB. When uploading to OneDrive or SharePoint, this value needs to be a multiple of 320 KiB (327,680 bytes). /// If less than 0, default value of 5 MiB is used. /// to use for making upload requests. The client should not set Auth headers as upload urls do not need them. - public LargeFileUploadTask(IParsable uploadSession, Stream uploadStream, int maxSliceSize = -1, IRequestAdapter requestAdapter = null) + [Obsolete("Use the overload that takes in IUploadSession instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public LargeFileUploadTask(IParsable uploadSession, Stream uploadStream, int maxSliceSize = -1, + IRequestAdapter requestAdapter = null) : this(ExtractSessionFromParsable(uploadSession), uploadStream, maxSliceSize, requestAdapter) + { + } + + /// + /// Task to help with resumable large file uploads. Generates slices based on + /// information, and can control uploading of requests. + /// + /// Session information of type > + /// Readable, seekable stream to be uploaded. Length of session is determined via uploadStream.Length + /// Max size(in bytes) of each slice to be uploaded. Defaults to 5MB. When uploading to OneDrive or SharePoint, this value needs to be a multiple of 320 KiB (327,680 bytes). + /// If less than 0, default value of 5 MiB is used. + /// to use for making upload requests. The client should not set Auth headers as upload urls do not need them. + public LargeFileUploadTask(IUploadSession uploadSession, Stream uploadStream, int maxSliceSize = -1, IRequestAdapter requestAdapter = null) { if (!uploadStream.CanRead || !uploadStream.CanSeek) { @@ -50,8 +69,8 @@ public LargeFileUploadTask(IParsable uploadSession, Stream uploadStream, int max { throw new ArgumentException("Must a stream that is not empty"); } - this.Session = ExtractSessionFromParsable(uploadSession); - this._requestAdapter = requestAdapter ?? this.InitializeAdapter(Session.UploadUrl); + this.Session = uploadSession; + this._requestAdapter = requestAdapter ?? InitializeAdapter(Session.UploadUrl); this._uploadStream = uploadStream; this._rangesRemaining = this.GetRangesRemaining(Session); this._maxSliceSize = maxSliceSize < 0 ? DefaultMaxSliceSize : maxSliceSize; @@ -63,23 +82,26 @@ public LargeFileUploadTask(IParsable uploadSession, Stream uploadStream, int max /// to initialize an from /// A instance /// - private IUploadSession ExtractSessionFromParsable(IParsable uploadSession) + internal static IUploadSession ExtractSessionFromParsable(IParsable uploadSession) { - if (!uploadSession.GetFieldDeserializers().ContainsKey("expirationDateTime")) + if (uploadSession is IUploadSession uploadSessionCast) + { + return uploadSessionCast; + } + + // this scenario (unlikely) will occur in the event the model in the service libraries hasn't been updated to implement the IUploadSession interface from core. + var fieldDeserializers = uploadSession.GetFieldDeserializers(); + if (!fieldDeserializers.ContainsKey("expirationDateTime")) throw new ArgumentException("The Parsable does not contain the 'expirationDateTime' property"); - if (!uploadSession.GetFieldDeserializers().ContainsKey("nextExpectedRanges")) + if (!fieldDeserializers.ContainsKey("nextExpectedRanges")) throw new ArgumentException("The Parsable does not contain the 'nextExpectedRanges' property"); - if (!uploadSession.GetFieldDeserializers().ContainsKey("uploadUrl")) + if (!fieldDeserializers.ContainsKey("uploadUrl")) throw new ArgumentException("The Parsable does not contain the 'uploadUrl' property"); - var uploadSessionType = uploadSession.GetType(); - - return new UploadSession() - { - ExpirationDateTime = uploadSessionType.GetProperty("ExpirationDateTime").GetValue(uploadSession, null) as DateTimeOffset?, - NextExpectedRanges = uploadSessionType.GetProperty("NextExpectedRanges").GetValue(uploadSession, null) as List, - UploadUrl = uploadSessionType.GetProperty("UploadUrl").GetValue(uploadSession, null) as string - }; + // convert to local type as we don't have the type info for the upload session just that it implements IParsable + using var uploadSessionStream = KiotaJsonSerializer.SerializeAsStream(uploadSession, false);// just in case there's a backing store + var uploadSessionJsonNode = new JsonParseNode(JsonDocument.Parse(uploadSessionStream).RootElement); + return uploadSessionJsonNode.GetObjectValue(UploadSession.CreateFromDiscriminatorValue); } /// @@ -87,7 +109,7 @@ private IUploadSession ExtractSessionFromParsable(IParsable uploadSession) /// /// Url to perform the upload to from the session /// - private IRequestAdapter InitializeAdapter(string uploadUrl) + private static BaseGraphRequestAdapter InitializeAdapter(string uploadUrl) { HttpClient httpClient = GraphClientFactory.Create(); //no auth httpClient.SetFeatureFlag(FeatureFlag.FileUploadTask); @@ -196,7 +218,7 @@ public async Task> UploadAsync(IProgress progress = null, return uploadResult; } - ThrowIfUploadCancelled(cancellationToken, trackedExceptions); + ThrowIfUploadCancelled(trackedExceptions, cancellationToken); } await this.UpdateSessionStatusAsync(cancellationToken).ConfigureAwait(false); @@ -207,7 +229,7 @@ public async Task> UploadAsync(IProgress progress = null, await Task.Delay(2000 * uploadTries * uploadTries, cancellationToken).ConfigureAwait(false); } - ThrowIfUploadCancelled(cancellationToken, trackedExceptions); + ThrowIfUploadCancelled(trackedExceptions, cancellationToken); } throw new TaskCanceledException("Upload failed too many times. See InnerException for list of exceptions that occured.", new AggregateException(trackedExceptions.ToArray())); @@ -292,7 +314,7 @@ private long NextSliceSize(long rangeBegin, long rangeEnd) : sizeBasedOnRange; } - private void ThrowIfUploadCancelled(CancellationToken cancellationToken, ICollection trackedExceptions) + private static void ThrowIfUploadCancelled(ICollection trackedExceptions, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) throw new OperationCanceledException("File upload cancelled. See InnerException for list of exceptions that occured.", new AggregateException(trackedExceptions)); diff --git a/src/Microsoft.Graph.Core/Tasks/PageIterator.cs b/src/Microsoft.Graph.Core/Tasks/PageIterator.cs index 4859860ca..f13bb116b 100644 --- a/src/Microsoft.Graph.Core/Tasks/PageIterator.cs +++ b/src/Microsoft.Graph.Core/Tasks/PageIterator.cs @@ -10,6 +10,9 @@ namespace Microsoft.Graph using System.Threading.Tasks; using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Serialization; +#if NET5_0_OR_GREATER + using System.Diagnostics.CodeAnalysis; +#endif /* Spec https://github.com/microsoftgraph/msgraph-sdk-design/blob/main/tasks/PageIteratorTask.md @@ -21,7 +24,11 @@ namespace Microsoft.Graph /// /// The Microsoft Graph entity type returned in the result set. /// The Microsoft Graph collection response type returned in the collection response. +#if NET5_0_OR_GREATER + public class PageIterator where TCollectionPage : IParsable, IAdditionalDataHolder, new() +#else public class PageIterator where TCollectionPage : IParsable, IAdditionalDataHolder, new() +#endif { private IRequestAdapter _requestAdapter; private TCollectionPage _currentPage; @@ -364,7 +371,7 @@ public async Task ResumeAsync(CancellationToken token) /// Thrown when the object doesn't contain a collection inside it private static List ExtractEntityListFromParsable(TCollectionPage parsableCollection) { - return parsableCollection.GetType().GetProperty("Value")?.GetValue(parsableCollection, null) as List ?? throw new ArgumentException("The Parsable does not contain a collection property"); + return typeof(TCollectionPage).GetProperty("Value")?.GetValue(parsableCollection, null) as List ?? throw new ArgumentException("The Parsable does not contain a collection property"); } /// @@ -375,7 +382,7 @@ private static List ExtractEntityListFromParsable(TCollectionPage parsa /// private static string ExtractNextLinkFromParsable(TCollectionPage parsableCollection, string nextLinkPropertyName = "OdataNextLink") { - var nextLinkProperty = parsableCollection.GetType().GetProperty(nextLinkPropertyName); + var nextLinkProperty = typeof(TCollectionPage).GetProperty(nextLinkPropertyName); if (nextLinkProperty != null && nextLinkProperty.GetValue(parsableCollection, null) is string nextLinkString && !string.IsNullOrEmpty(nextLinkString)) diff --git a/tests/Microsoft.Graph.DotnetCore.Core.Test/Mocks/MockUploadSessionWithoutInterface.cs b/tests/Microsoft.Graph.DotnetCore.Core.Test/Mocks/MockUploadSessionWithoutInterface.cs new file mode 100644 index 000000000..9c9a861d1 --- /dev/null +++ b/tests/Microsoft.Graph.DotnetCore.Core.Test/Mocks/MockUploadSessionWithoutInterface.cs @@ -0,0 +1,78 @@ +namespace Microsoft.Graph.DotnetCore.Core.Test.Mocks +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Microsoft.Kiota.Abstractions.Serialization; + + /// + /// Concrete implementation of the IUploadSession interface + /// + internal class MockUploadSessionWithoutUploadSessionInterface : IParsable, IAdditionalDataHolder + { + /// + /// Expiration date of the upload session + /// + public DateTimeOffset? ExpirationDateTime + { + get; set; + } + + /// + /// The ranges yet to be uploaded to the server + /// + public List NextExpectedRanges + { + get; set; + } + + /// + /// The URL for upload + /// + public string UploadUrl + { + get; set; + } + + /// + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + /// + public IDictionary AdditionalData { get; set; } = new Dictionary(); + + /// + /// The deserialization information for the current model + /// + public IDictionary> GetFieldDeserializers() + { + return new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + {"expirationDateTime", (n) => { ExpirationDateTime = n.GetDateTimeOffsetValue(); } }, + {"nextExpectedRanges", (n) => { NextExpectedRanges = n.GetCollectionOfPrimitiveValues().ToList(); } }, + {"uploadUrl", (n) => { UploadUrl = n.GetStringValue(); } }, + }; + } + + /// + /// Serializes information the current object + /// Serialization writer to use to serialize this model + /// + public void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteDateTimeOffsetValue("expirationDateTime", ExpirationDateTime); + writer.WriteCollectionOfPrimitiveValues("nextExpectedRanges", NextExpectedRanges); + writer.WriteStringValue("uploadUrl", UploadUrl); + writer.WriteAdditionalData(AdditionalData); + } + + /// + /// Creates a new instance of the appropriate class based on discriminator value + /// The parse node to use to read the discriminator value and create the object + /// + public static MockUploadSessionWithoutUploadSessionInterface CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new MockUploadSessionWithoutUploadSessionInterface(); + } + } +} diff --git a/tests/Microsoft.Graph.DotnetCore.Core.Test/Tasks/LargeFileUploadTaskTests.cs b/tests/Microsoft.Graph.DotnetCore.Core.Test/Tasks/LargeFileUploadTaskTests.cs index 745d626bd..c40e5a9a6 100644 --- a/tests/Microsoft.Graph.DotnetCore.Core.Test/Tasks/LargeFileUploadTaskTests.cs +++ b/tests/Microsoft.Graph.DotnetCore.Core.Test/Tasks/LargeFileUploadTaskTests.cs @@ -27,6 +27,30 @@ public LargeFileUploadTests() { // register the default serialization instance as the generator would. ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories.TryAdd(CoreConstants.MimeTypeNames.Application.Json, new JsonParseNodeFactory()); + SerializationWriterFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories.TryAdd(CoreConstants.MimeTypeNames.Application.Json, new JsonSerializationWriterFactory()); + } + + [Fact] + public void ObsoleteMethodWorksWithIParsable() + { + var uploadSession = new MockUploadSessionWithoutUploadSessionInterface + { + NextExpectedRanges = new List() { "0-" }, + UploadUrl = "http://localhost", + ExpirationDateTime = DateTimeOffset.Parse("2019-11-07T06:39:31.499Z") + }; + + int maxSliceSize = 200 * 1024;//slice size that is 200 KB + + // Act + var exception = Assert.Throws(() => + { + using Stream stream = new MemoryStream(); +#pragma warning disable CS0618 // Type or member is obsolete + return new LargeFileUploadTask(uploadSession, stream, maxSliceSize); +#pragma warning restore CS0618 // Type or member is obsolete + }); + Assert.NotNull(exception); } [Fact] @@ -51,6 +75,42 @@ public void ThrowsOnEmptyStream() Assert.NotNull(exception); } [Fact] + public void ParsesUploadSessionWithoutSerialization() + { + + var uploadSession = new UploadSession + { + NextExpectedRanges = new List() { "0-" }, + UploadUrl = "http://localhost", + ExpirationDateTime = DateTimeOffset.Parse("2019-11-07T06:39:31.499Z") + }; + + var parsedSession = LargeFileUploadTask.ExtractSessionFromParsable(uploadSession); + + Assert.Equal(uploadSession.UploadUrl, parsedSession.UploadUrl); + Assert.Equal(uploadSession.ExpirationDateTime, parsedSession.ExpirationDateTime); + Assert.Equal(uploadSession.NextExpectedRanges.Count, parsedSession.NextExpectedRanges.Count); + Assert.Equal(uploadSession.NextExpectedRanges[0], parsedSession.NextExpectedRanges[0]); + } + [Fact] + public void ParsesUploadSessionWithSerializationForModelsWithoutInterface() + { + + var uploadSession = new MockUploadSessionWithoutUploadSessionInterface + { + NextExpectedRanges = new List() { "0-" }, + UploadUrl = "http://localhost", + ExpirationDateTime = DateTimeOffset.Parse("2019-11-07T06:39:31.499Z") + }; + + var parsedSession = LargeFileUploadTask.ExtractSessionFromParsable(uploadSession); + + Assert.Equal(uploadSession.UploadUrl, parsedSession.UploadUrl); + Assert.Equal(uploadSession.ExpirationDateTime, parsedSession.ExpirationDateTime); + Assert.Equal(uploadSession.NextExpectedRanges.Count, parsedSession.NextExpectedRanges.Count); + Assert.Equal(uploadSession.NextExpectedRanges[0], parsedSession.NextExpectedRanges[0]); + } + [Fact] public void AllowsVariableSliceSize() { byte[] mockData = new byte[1000000]; @@ -117,28 +177,43 @@ public void BreaksDownStreamIntoRangesCorrectly() ExpirationDateTime = DateTimeOffset.Parse("2019-11-07T06:39:31.499Z") }; + var uploadSessionWithoutInterface = new MockUploadSessionWithoutUploadSessionInterface() + { + NextExpectedRanges = new List() { "0-" }, + UploadUrl = "http://localhost", + ExpirationDateTime = DateTimeOffset.Parse("2019-11-07T06:39:31.499Z") + }; + int maxSliceSize = 320 * 1024; // Act - var fileUploadTask = new LargeFileUploadTask(uploadSession, stream, maxSliceSize); - var uploadSlices = fileUploadTask.GetUploadSliceRequests(); + var fileUploadTaskWithInterface = new LargeFileUploadTask(uploadSession, stream, maxSliceSize); +#pragma warning disable CS0618 // Type or member is obsolete + var fileUploadTaskWithObsoleteMember = new LargeFileUploadTask(uploadSessionWithoutInterface, stream, maxSliceSize); +#pragma warning restore CS0618 // Type or member is obsolete + var tasks = new[] { fileUploadTaskWithInterface, fileUploadTaskWithObsoleteMember }; - // Assert - //We have only 4 slices - Assert.Equal(4, uploadSlices.Count()); - - long currentRangeBegins = 0; - foreach (var uploadSlice in uploadSlices) + foreach (var fileUploadTask in tasks) { - Assert.Equal(stream.Length, uploadSlice.TotalSessionLength); - Assert.Equal(currentRangeBegins, uploadSlice.RangeBegin); - currentRangeBegins += maxSliceSize; - } + var uploadSlices = fileUploadTask.GetUploadSliceRequests().ToArray(); - //The last slice is a a bit smaller than the rest - var lastUploadSlice = uploadSlices.Last(); - Assert.Equal(stream.Length - 1, lastUploadSlice.RangeEnd); - Assert.Equal(stream.Length % maxSliceSize, lastUploadSlice.RangeLength); //verify the last slice is the right size + // Assert + //We have only 4 slices + Assert.Equal(4, uploadSlices.Length); + + long currentRangeBegins = 0; + foreach (var uploadSlice in uploadSlices) + { + Assert.Equal(stream.Length, uploadSlice.TotalSessionLength); + Assert.Equal(currentRangeBegins, uploadSlice.RangeBegin); + currentRangeBegins += maxSliceSize; + } + + //The last slice is a bit smaller than the rest + var lastUploadSlice = uploadSlices[^1]; + Assert.Equal(stream.Length - 1, lastUploadSlice.RangeEnd); + Assert.Equal(stream.Length % maxSliceSize, lastUploadSlice.RangeLength); //verify the last slice is the right size + } } [Fact] diff --git a/tests/Microsoft.Graph.DotnetCore.Core.Trimming/Microsoft.Graph.DotnetCore.Core.Trimming.csproj b/tests/Microsoft.Graph.DotnetCore.Core.Trimming/Microsoft.Graph.DotnetCore.Core.Trimming.csproj new file mode 100644 index 000000000..c35b98381 --- /dev/null +++ b/tests/Microsoft.Graph.DotnetCore.Core.Trimming/Microsoft.Graph.DotnetCore.Core.Trimming.csproj @@ -0,0 +1,17 @@ + + + Exe + net8.0 + enable + enable + true + true + false + true + true + + + + + + diff --git a/tests/Microsoft.Graph.DotnetCore.Core.Trimming/Program.cs b/tests/Microsoft.Graph.DotnetCore.Core.Trimming/Program.cs new file mode 100644 index 000000000..47cd26fb1 --- /dev/null +++ b/tests/Microsoft.Graph.DotnetCore.Core.Trimming/Program.cs @@ -0,0 +1,9 @@ +namespace Microsoft.Graph.DotnetCore.Core.Trimming; + +class Program +{ + static void Main(string[] args) + { + Console.WriteLine("Hello, World!"); + } +} diff --git a/tests/Microsoft.Graph.DotnetCore.Core.Trimming/global.json b/tests/Microsoft.Graph.DotnetCore.Core.Trimming/global.json new file mode 100644 index 000000000..d251ac4e7 --- /dev/null +++ b/tests/Microsoft.Graph.DotnetCore.Core.Trimming/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.101", /* https://github.com/dotnet/maui/wiki/.NET-7-and-.NET-MAUI */ + "rollForward": "major" + } +}