diff --git a/CHANGELOG.md b/CHANGELOG.md index cf15f234..c2f8072a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.6.0] - 2023-10-31 + +### Added + +- Added helper methods to facilitate serialization and deserialization of models. [microsoft/kiota#3406](https://github.com/microsoft/kiota/issues/3406) + ## [1.5.0] - 2023-10-19 ### Added diff --git a/Microsoft.Kiota.Abstractions.Tests/ApiClientBuilderTests.cs b/Microsoft.Kiota.Abstractions.Tests/ApiClientBuilderTests.cs index 7da19e0a..d377feb4 100644 --- a/Microsoft.Kiota.Abstractions.Tests/ApiClientBuilderTests.cs +++ b/Microsoft.Kiota.Abstractions.Tests/ApiClientBuilderTests.cs @@ -8,7 +8,7 @@ namespace Microsoft.Kiota.Abstractions.Tests public class ApiClientBuilderTests { private const string StreamContentType = "application/octet-stream"; - + [Fact] public void EnableBackingStoreForSerializationWriterFactory() { @@ -16,12 +16,12 @@ public void EnableBackingStoreForSerializationWriterFactory() var serializationFactoryRegistry = new SerializationWriterFactoryRegistry(); var mockSerializationWriterFactory = new Mock(); serializationFactoryRegistry.ContentTypeAssociatedFactories.TryAdd(StreamContentType, mockSerializationWriterFactory.Object); - + Assert.IsNotType(serializationFactoryRegistry.ContentTypeAssociatedFactories[StreamContentType]); - + // Act ApiClientBuilder.EnableBackingStoreForSerializationWriterFactory(serializationFactoryRegistry); - + // Assert the type has changed due to backing store enabling Assert.IsType(serializationFactoryRegistry.ContentTypeAssociatedFactories[StreamContentType]); } @@ -52,12 +52,12 @@ public void EnableBackingStoreForParseNodeFactory() var parseNodeRegistry = new ParseNodeFactoryRegistry(); var mockParseNodeFactory = new Mock(); parseNodeRegistry.ContentTypeAssociatedFactories.TryAdd(StreamContentType, mockParseNodeFactory.Object); - + Assert.IsNotType(parseNodeRegistry.ContentTypeAssociatedFactories[StreamContentType]); - + // Act ApiClientBuilder.EnableBackingStoreForParseNodeFactory(parseNodeRegistry); - + // Assert the type has changed due to backing store enabling Assert.IsType(parseNodeRegistry.ContentTypeAssociatedFactories[StreamContentType]); } diff --git a/Microsoft.Kiota.Abstractions.Tests/Authentication/ApiKeyAuthenticationProviderTests.cs b/Microsoft.Kiota.Abstractions.Tests/Authentication/ApiKeyAuthenticationProviderTests.cs index f9e236fe..8473a35a 100644 --- a/Microsoft.Kiota.Abstractions.Tests/Authentication/ApiKeyAuthenticationProviderTests.cs +++ b/Microsoft.Kiota.Abstractions.Tests/Authentication/ApiKeyAuthenticationProviderTests.cs @@ -5,9 +5,11 @@ namespace Microsoft.Kiota.Abstractions.Tests; -public class ApiKeyAuthenticationProviderTests { +public class ApiKeyAuthenticationProviderTests +{ [Fact] - public void DefensiveProgramming() { + public void DefensiveProgramming() + { Assert.Throws(() => new ApiKeyAuthenticationProvider(null, "param", ApiKeyAuthenticationProvider.KeyLocation.Header)); Assert.Throws(() => new ApiKeyAuthenticationProvider("key", null, ApiKeyAuthenticationProvider.KeyLocation.Header)); @@ -15,9 +17,11 @@ public void DefensiveProgramming() { Assert.ThrowsAsync(async () => await value.AuthenticateRequestAsync(null)); } [Fact] - public async Task AddsInHeader() { + public async Task AddsInHeader() + { var value = new ApiKeyAuthenticationProvider("key", "param", ApiKeyAuthenticationProvider.KeyLocation.Header); - var request = new RequestInformation { + var request = new RequestInformation + { UrlTemplate = "https://localhost{?param1}", }; await value.AuthenticateRequestAsync(request); @@ -25,9 +29,11 @@ public async Task AddsInHeader() { Assert.Contains("param", request.Headers.Keys); } [Fact] - public async Task AddsInQueryParameters() { + public async Task AddsInQueryParameters() + { var value = new ApiKeyAuthenticationProvider("key", "param", ApiKeyAuthenticationProvider.KeyLocation.QueryParameter); - var request = new RequestInformation { + var request = new RequestInformation + { UrlTemplate = "https://localhost{?param1}", }; await value.AuthenticateRequestAsync(request); @@ -35,9 +41,11 @@ public async Task AddsInQueryParameters() { Assert.DoesNotContain("param", request.Headers.Keys); } [Fact] - public async Task AddsInQueryParametersWithOtherParameters() { + public async Task AddsInQueryParametersWithOtherParameters() + { var value = new ApiKeyAuthenticationProvider("key", "param", ApiKeyAuthenticationProvider.KeyLocation.QueryParameter); - var request = new RequestInformation { + var request = new RequestInformation + { UrlTemplate = "https://localhost{?param1}", }; request.QueryParameters.Add("param1", "value1"); diff --git a/Microsoft.Kiota.Abstractions.Tests/Authentication/AuthenticationTests.cs b/Microsoft.Kiota.Abstractions.Tests/Authentication/AuthenticationTests.cs index e84021d8..78c0a567 100644 --- a/Microsoft.Kiota.Abstractions.Tests/Authentication/AuthenticationTests.cs +++ b/Microsoft.Kiota.Abstractions.Tests/Authentication/AuthenticationTests.cs @@ -55,16 +55,16 @@ public async Task BaseBearerTokenAuthenticationProviderSetsBearerHeader() } [Theory] - [InlineData("https://graph.microsoft.com",true)]// PASS - [InlineData("https://graph.microsoft.us/v1.0/me",true)]// PASS as we don't look at the path segment - [InlineData("https://test.microsoft.com",false)]// Fail - [InlineData("https://grAph.MicrosofT.com",true)] // PASS since we don't care about case - [InlineData("https://developer.microsoft.com",false)] // Failed + [InlineData("https://graph.microsoft.com", true)]// PASS + [InlineData("https://graph.microsoft.us/v1.0/me", true)]// PASS as we don't look at the path segment + [InlineData("https://test.microsoft.com", false)]// Fail + [InlineData("https://grAph.MicrosofT.com", true)] // PASS since we don't care about case + [InlineData("https://developer.microsoft.com", false)] // Failed public void AllowedHostValidatorValidatesUrls(string urlToTest, bool expectedResult) { // Test through the constructor // Arrange - var allowList = new[] { "graph.microsoft.com", "graph.microsoft.us"}; + var allowList = new[] { "graph.microsoft.com", "graph.microsoft.us" }; var validator = new AllowedHostsValidator(allowList); // Act diff --git a/Microsoft.Kiota.Abstractions.Tests/Serialization/DeserializationHelpersTests.cs b/Microsoft.Kiota.Abstractions.Tests/Serialization/DeserializationHelpersTests.cs new file mode 100644 index 00000000..272d1f4a --- /dev/null +++ b/Microsoft.Kiota.Abstractions.Tests/Serialization/DeserializationHelpersTests.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions.Tests.Mocks; +using Moq; +using Xunit; + +namespace Microsoft.Kiota.Abstractions.Tests.Serialization; + +public class DeserializationHelpersTests +{ + private const string _jsonContentType = "application/json"; + [Fact] + public void DefensiveObject() + { + Assert.Throws(() => KiotaSerializer.Deserialize(null, (Stream)null, null)); + Assert.Throws(() => KiotaSerializer.Deserialize(_jsonContentType, (Stream)null, null)); + using var stream = new MemoryStream(); + Assert.Throws(() => KiotaSerializer.Deserialize(_jsonContentType, stream, null)); + Assert.Throws(() => KiotaSerializer.Deserialize(_jsonContentType, "", null)); + } + [Fact] + public void DefensiveObjectCollection() + { + Assert.Throws(() => KiotaSerializer.DeserializeCollection(null, (Stream)null, null)); + Assert.Throws(() => KiotaSerializer.DeserializeCollection(_jsonContentType, (Stream)null, null)); + using var stream = new MemoryStream(); + Assert.Throws(() => KiotaSerializer.DeserializeCollection(_jsonContentType, stream, null)); + Assert.Throws(() => KiotaSerializer.DeserializeCollection(_jsonContentType, "", null)); + } + [Fact] + public void DeserializesObjectWithoutReflection() + { + var strValue = "{'id':'123'}"; + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetObjectValue(It.IsAny>())).Returns(new TestEntity() + { + Id = "123" + }); + var mockJsonParseNodeFactory = new Mock(); + mockJsonParseNodeFactory.Setup(x => x.GetRootParseNode(It.IsAny(), It.IsAny())).Returns(mockParseNode.Object); + mockJsonParseNodeFactory.Setup(x => x.ValidContentType).Returns(_jsonContentType); + ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockJsonParseNodeFactory.Object; + + var result = KiotaSerializer.Deserialize(_jsonContentType, strValue, TestEntity.CreateFromDiscriminatorValue); + + Assert.NotNull(result); + } + [Fact] + public void DeserializesObjectWithReflection() + { + var strValue = "{'id':'123'}"; + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetObjectValue(It.IsAny>())).Returns(new TestEntity() + { + Id = "123" + }); + var mockJsonParseNodeFactory = new Mock(); + mockJsonParseNodeFactory.Setup(x => x.GetRootParseNode(It.IsAny(), It.IsAny())).Returns(mockParseNode.Object); + mockJsonParseNodeFactory.Setup(x => x.ValidContentType).Returns(_jsonContentType); + ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockJsonParseNodeFactory.Object; + + var result = KiotaSerializer.Deserialize(_jsonContentType, strValue); + + Assert.NotNull(result); + } + [Fact] + public void DeserializesCollectionOfObject() + { + var strValue = "{'id':'123'}"; + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetCollectionOfObjectValues(It.IsAny>())).Returns(new List { + new TestEntity() + { + Id = "123" + } + }); + var mockJsonParseNodeFactory = new Mock(); + mockJsonParseNodeFactory.Setup(x => x.GetRootParseNode(It.IsAny(), It.IsAny())).Returns(mockParseNode.Object); + mockJsonParseNodeFactory.Setup(x => x.ValidContentType).Returns(_jsonContentType); + ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockJsonParseNodeFactory.Object; + + var result = KiotaSerializer.DeserializeCollection(_jsonContentType, strValue, TestEntity.CreateFromDiscriminatorValue); + + Assert.NotNull(result); + Assert.Single(result); + } +} diff --git a/Microsoft.Kiota.Abstractions.Tests/Serialization/Mocks/TestEntity.cs b/Microsoft.Kiota.Abstractions.Tests/Serialization/Mocks/TestEntity.cs new file mode 100644 index 00000000..958ddca4 --- /dev/null +++ b/Microsoft.Kiota.Abstractions.Tests/Serialization/Mocks/TestEntity.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using Microsoft.Kiota.Abstractions.Serialization; + +namespace Microsoft.Kiota.Abstractions.Tests.Serialization.Mocks +{ + public class TestEntity : IParsable, IAdditionalDataHolder + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData { get; set; } + /// Read-only. + public string Id { get; set; } + /// Read-only. + public TimeSpan? WorkDuration { get; set; } + /// Read-only. + public Date? BirthDay { get; set; } + /// Read-only. + public Time? StartWorkTime { get; set; } + /// Read-only. + public Time? EndWorkTime { get; set; } + /// Read-only. + public DateTimeOffset? CreatedDateTime { get; set; } + /// Read-only. + public string OfficeLocation { get; set; } + /// + /// Instantiates a new entity and sets the default values. + /// + public TestEntity() + { + AdditionalData = new Dictionary(); + } + /// + /// The deserialization information for the current model + /// + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> { + {"id", n => { Id = n.GetStringValue(); } }, + {"createdDateTime", n => { CreatedDateTime = n.GetDateTimeOffsetValue(); } }, + {"officeLocation", n => { OfficeLocation = n.GetStringValue(); } }, + {"workDuration", n => { WorkDuration = n.GetTimeSpanValue(); } }, + {"birthDay", n => { BirthDay = n.GetDateValue(); } }, + {"startWorkTime", n => { StartWorkTime = n.GetTimeValue(); } }, + {"endWorkTime", n => { EndWorkTime = n.GetTimeValue(); } }, + }; + } + /// + /// Serializes information the current object + /// Serialization writer to use to serialize this model + /// + public virtual void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("id", Id); + writer.WriteDateTimeOffsetValue("createdDateTime", CreatedDateTime); + writer.WriteStringValue("officeLocation", OfficeLocation); + writer.WriteTimeSpanValue("workDuration", WorkDuration); + writer.WriteDateValue("birthDay", BirthDay); + writer.WriteTimeValue("startWorkTime", StartWorkTime); + writer.WriteTimeValue("endWorkTime", EndWorkTime); + writer.WriteAdditionalData(AdditionalData); + } + public static TestEntity CreateFromDiscriminator(IParseNode parseNode) + { + var discriminatorValue = parseNode.GetChildNode("@odata.type")?.GetStringValue(); + return discriminatorValue switch + { + "microsoft.graph.user" => new TestEntity(), + "microsoft.graph.group" => new TestEntity(), + _ => new TestEntity(), + }; + } + } +} diff --git a/Microsoft.Kiota.Abstractions.Tests/Serialization/SerializationHelpersTests.cs b/Microsoft.Kiota.Abstractions.Tests/Serialization/SerializationHelpersTests.cs new file mode 100644 index 00000000..79d8e2a9 --- /dev/null +++ b/Microsoft.Kiota.Abstractions.Tests/Serialization/SerializationHelpersTests.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions.Tests.Mocks; +using Moq; +using Xunit; + +namespace Microsoft.Kiota.Abstractions.Tests.Serialization; + +public class SerializationHelpersTests +{ + private const string _jsonContentType = "application/json"; + [Fact] + public void DefensiveObject() + { + Assert.Throws(() => KiotaSerializer.SerializeAsStream(null, (TestEntity)null)); + Assert.Throws(() => KiotaSerializer.SerializeAsStream(_jsonContentType, (TestEntity)null)); + } + [Fact] + public void DefensiveObjectCollection() + { + Assert.Throws(() => KiotaSerializer.SerializeAsStream(null, (IEnumerable)null)); + Assert.Throws(() => KiotaSerializer.SerializeAsStream(_jsonContentType, (IEnumerable)null)); + } + [Fact] + public void SerializesObject() + { + var mockSerializationWriter = new Mock(); + mockSerializationWriter.Setup(x => x.GetSerializedContent()).Returns(new MemoryStream(UTF8Encoding.UTF8.GetBytes("{'id':'123'}"))); + var mockSerializationWriterFactory = new Mock(); + mockSerializationWriterFactory.Setup(x => x.GetSerializationWriter(It.IsAny())).Returns(mockSerializationWriter.Object); + SerializationWriterFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockSerializationWriterFactory.Object; + + var result = KiotaSerializer.SerializeAsString(_jsonContentType, new TestEntity() + { + Id = "123" + }); + + Assert.Equal("{'id':'123'}", result); + + mockSerializationWriterFactory.Verify(x => x.GetSerializationWriter(It.IsAny()), Times.Once); + mockSerializationWriter.Verify(x => x.WriteObjectValue(It.IsAny(), It.IsAny()), Times.Once); + mockSerializationWriter.Verify(x => x.GetSerializedContent(), Times.Once); + } + [Fact] + public void SerializesObjectCollection() + { + var mockSerializationWriter = new Mock(); + mockSerializationWriter.Setup(x => x.GetSerializedContent()).Returns(new MemoryStream(UTF8Encoding.UTF8.GetBytes("[{'id':'123'}]"))); + var mockSerializationWriterFactory = new Mock(); + mockSerializationWriterFactory.Setup(x => x.GetSerializationWriter(It.IsAny())).Returns(mockSerializationWriter.Object); + SerializationWriterFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockSerializationWriterFactory.Object; + + var result = KiotaSerializer.SerializeAsString(_jsonContentType, new List { + new() + { + Id = "123" + } + }); + + Assert.Equal("[{'id':'123'}]", result); + + mockSerializationWriterFactory.Verify(x => x.GetSerializationWriter(It.IsAny()), Times.Once); + mockSerializationWriter.Verify(x => x.WriteCollectionOfObjectValues(It.IsAny(), It.IsAny>()), Times.Once); + mockSerializationWriter.Verify(x => x.GetSerializedContent(), Times.Once); + } +} \ No newline at end of file diff --git a/Microsoft.Kiota.Abstractions.Tests/Store/InMemoryBackingStoreTests.cs b/Microsoft.Kiota.Abstractions.Tests/Store/InMemoryBackingStoreTests.cs index 7be78cdb..0758b56f 100644 --- a/Microsoft.Kiota.Abstractions.Tests/Store/InMemoryBackingStoreTests.cs +++ b/Microsoft.Kiota.Abstractions.Tests/Store/InMemoryBackingStoreTests.cs @@ -21,7 +21,7 @@ public void SetsAndGetsValueFromStore() testBackingStore.Set("name", "Peter"); // Assert Assert.NotEmpty(testBackingStore.Enumerate()); - Assert.Equal("Peter",testBackingStore.Enumerate().First().Value); + Assert.Equal("Peter", testBackingStore.Enumerate().First().Value); } [Fact] @@ -66,7 +66,7 @@ public void TestsBackingStoreEmbeddedInModel() }; testUser.BackingStore.InitializationCompleted = true; // Act on the data by making a change - testUser.BusinessPhones = new List + testUser.BusinessPhones = new List { "+1 234 567 891" }; @@ -84,7 +84,7 @@ public void TestsBackingStoreEmbeddedInModelWithAdditionDataValues() var testUser = new TestEntity { Id = "84c747c1-d2c0-410d-ba50-fc23e0b4abbe", - AdditionalData = new Dictionary + AdditionalData = new Dictionary { { "extensionData" , null } } @@ -182,7 +182,7 @@ public void TestsBackingStoreEmbeddedInModelWithCollectionPropertyModifiedByAdd( Assert.Equal("businessPhones", changedValues.First().Key); var businessPhones = testUser.BackingStore.Get>("businessPhones"); Assert.NotNull(businessPhones); - Assert.Equal(2,businessPhones.Count);//both items come back as the property is dirty + Assert.Equal(2, businessPhones.Count);//both items come back as the property is dirty } [Fact] public void TestsBackingStoreEmbeddedInModelWithBySettingNestedIBackedModel() @@ -194,7 +194,7 @@ public void TestsBackingStoreEmbeddedInModelWithBySettingNestedIBackedModel() }; testUser.BackingStore.InitializationCompleted = true; // Act on the data by making a change - testUser.Manager = new TestEntity + testUser.Manager = new TestEntity { Id = "2fe22fe5-1132-42cf-90f9-1dc17e325a74" }; @@ -206,7 +206,7 @@ public void TestsBackingStoreEmbeddedInModelWithBySettingNestedIBackedModel() Assert.Equal("manager", changedValues.First().Key); var manager = changedValues.First().Value as TestEntity; Assert.NotNull(manager); - Assert.Equal("2fe22fe5-1132-42cf-90f9-1dc17e325a74",manager.Id); + Assert.Equal("2fe22fe5-1132-42cf-90f9-1dc17e325a74", manager.Id); var testUserSubscriptions = GetSubscriptionsPropertyFromBackingStore(testUser.BackingStore); Assert.Empty(testUserSubscriptions);// subscription only is added in nested store var managerSubscriptions = GetSubscriptionsPropertyFromBackingStore(testUser.Manager.BackingStore); @@ -238,7 +238,7 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModel() Assert.Equal("manager", changedValues.First().Key);//Backingstore should detect manager property changed var manager = changedValues.First().Value as TestEntity; Assert.NotNull(manager); - Assert.Equal("2fe22fe5-1132-42cf-90f9-1dc17e325a74",manager.Id); + Assert.Equal("2fe22fe5-1132-42cf-90f9-1dc17e325a74", manager.Id); var testUserSubscriptions = GetSubscriptionsPropertyFromBackingStore(testUser.BackingStore); Assert.Empty(testUserSubscriptions);// subscription only is added in nested store var managerSubscriptions = GetSubscriptionsPropertyFromBackingStore(testUser.Manager.BackingStore); @@ -269,13 +269,13 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelRetu Assert.NotEmpty(changedValues); Assert.Single(changedValues); Assert.Equal("manager", changedValues.First().Key);//BackingStore should detect manager property changed - var changedNestedValues = testUser.Manager.BackingStore.Enumerate().ToDictionary(x => x.Key, y=> y.Value); - Assert.Equal(4,changedNestedValues.Count); + var changedNestedValues = testUser.Manager.BackingStore.Enumerate().ToDictionary(x => x.Key, y => y.Value); + Assert.Equal(4, changedNestedValues.Count); Assert.True(changedNestedValues.ContainsKey("id")); Assert.True(changedNestedValues.ContainsKey("businessPhones")); var manager = changedValues.First().Value as TestEntity; Assert.NotNull(manager); - Assert.Equal("2fe22fe5-1132-42cf-90f9-1dc17e325a74",manager.Id); + Assert.Equal("2fe22fe5-1132-42cf-90f9-1dc17e325a74", manager.Id); var testUserSubscriptions = GetSubscriptionsPropertyFromBackingStore(testUser.BackingStore); Assert.Empty(testUserSubscriptions);// subscription only is added in nested store var managerSubscriptions = GetSubscriptionsPropertyFromBackingStore(testUser.Manager.BackingStore); @@ -288,13 +288,13 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl var testUser = new TestEntity { Id = "84c747c1-d2c0-410d-ba50-fc23e0b4abbe", - Colleagues = new List + Colleagues = new List { new TestEntity { Id = "2fe22fe5-1132-42cf-90f9-1dc17e325a74" } - } + } }; testUser.BackingStore.InitializationCompleted = testUser.Colleagues[0].BackingStore.InitializationCompleted = true; // Act on the data by making a change in the nested Ibackedmodel collection item @@ -310,7 +310,7 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl Assert.Equal("colleagues", changedValues.First().Key);//Backingstore should detect manager property changed var colleagues = testUser.BackingStore.Get>("colleagues"); Assert.NotNull(colleagues); - Assert.Equal("2fe22fe5-1132-42cf-90f9-1dc17e325a74",colleagues[0].Id); + Assert.Equal("2fe22fe5-1132-42cf-90f9-1dc17e325a74", colleagues[0].Id); var testUserSubscriptions = GetSubscriptionsPropertyFromBackingStore(testUser.BackingStore); Assert.Empty(testUserSubscriptions);// subscription only is added in nested store var colleagueSubscriptions = GetSubscriptionsPropertyFromBackingStore(testUser.Colleagues[0].BackingStore); @@ -323,13 +323,13 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl var testUser = new TestEntity { Id = "84c747c1-d2c0-410d-ba50-fc23e0b4abbe", - Colleagues = new List + Colleagues = new List { new TestEntity { Id = "2fe22fe5-1132-42cf-90f9-1dc17e325a74" } - } + } }; testUser.BackingStore.InitializationCompleted = testUser.Colleagues[0].BackingStore.InitializationCompleted = true; // Act on the data by making a change in the nested Ibackedmodel collection item @@ -344,8 +344,8 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl Assert.NotEmpty(changedValues); Assert.Single(changedValues); Assert.Equal("colleagues", changedValues.First().Key);//BackingStore should detect manager property changed - var changedNestedValues = testUser.Colleagues[0].BackingStore.Enumerate().ToDictionary(x => x.Key, y=> y.Value); - Assert.Equal(4,changedNestedValues.Count); + var changedNestedValues = testUser.Colleagues[0].BackingStore.Enumerate().ToDictionary(x => x.Key, y => y.Value); + Assert.Equal(4, changedNestedValues.Count); Assert.True(changedNestedValues.ContainsKey("id")); Assert.True(changedNestedValues.ContainsKey("businessPhones")); var testUserSubscriptions = GetSubscriptionsPropertyFromBackingStore(testUser.BackingStore); @@ -360,7 +360,7 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl var testUser = new TestEntity { Id = "84c747c1-d2c0-410d-ba50-fc23e0b4abbe", - Colleagues = new List + Colleagues = new List { new TestEntity { @@ -370,7 +370,7 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl "+1 234 567 891" } } - } + } }; testUser.BackingStore.InitializationCompleted = testUser.Colleagues[0].BackingStore.InitializationCompleted = true; // Act on the data by making a change in the nested Ibackedmodel collection item @@ -382,8 +382,8 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl Assert.NotEmpty(changedValues); Assert.Single(changedValues); Assert.Equal("colleagues", changedValues.First().Key);//Backingstore should detect manager property changed - var changedNestedValues = testUser.Colleagues[0].BackingStore.Enumerate().ToDictionary(x => x.Key, y=> y.Value); - Assert.Equal(4,changedNestedValues.Count); + var changedNestedValues = testUser.Colleagues[0].BackingStore.Enumerate().ToDictionary(x => x.Key, y => y.Value); + Assert.Equal(4, changedNestedValues.Count); Assert.True(changedNestedValues.ContainsKey("id")); Assert.True(changedNestedValues.ContainsKey("businessPhones")); var businessPhones = ((Tuple)changedNestedValues["businessPhones"]).Item1; @@ -400,7 +400,7 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl var testUser = new TestEntity { Id = "84c747c1-d2c0-410d-ba50-fc23e0b4abbe", - Colleagues = new List + Colleagues = new List { new TestEntity { @@ -410,7 +410,7 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl "+1 234 567 891" } } - } + } }; testUser.BackingStore.InitializationCompleted = testUser.Colleagues[0].BackingStore.InitializationCompleted = true; // Act on the data by making a change in the nested Ibackedmodel collection item @@ -421,7 +421,7 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl // Assert by retrieving only changed values testUser.BackingStore.ReturnOnlyChangedValues = true; testUser.Colleagues[0].BackingStore.ReturnOnlyChangedValues = true; //serializer will do this. - var changedValues = testUser.BackingStore.Enumerate().ToDictionary(x => x.Key, y=> y.Value); + var changedValues = testUser.BackingStore.Enumerate().ToDictionary(x => x.Key, y => y.Value); Assert.NotEmpty(changedValues); Assert.Single(changedValues); Assert.Equal("colleagues", changedValues.First().Key);//Backingstore should detect manager property changed @@ -429,8 +429,8 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl Assert.Equal(2, colleagues.Count); Assert.Equal("2fe22fe5-1132-42cf-90f9-1dc17e325a74", colleagues[0].Id); Assert.Equal("2fe22fe5-1132-42cf-90f9-1dc17e325a74", colleagues[1].Id); - var changedNestedValues = testUser.Colleagues[0].BackingStore.Enumerate().ToDictionary(x => x.Key, y=> y.Value); - Assert.Equal(4,changedNestedValues.Count); + var changedNestedValues = testUser.Colleagues[0].BackingStore.Enumerate().ToDictionary(x => x.Key, y => y.Value); + Assert.Equal(4, changedNestedValues.Count); Assert.True(changedNestedValues.ContainsKey("id")); Assert.True(changedNestedValues.ContainsKey("businessPhones")); var businessPhones = ((Tuple)changedNestedValues["businessPhones"]).Item1; @@ -448,9 +448,9 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl /// private static IDictionary> GetSubscriptionsPropertyFromBackingStore(IBackingStore backingStore) { - if(backingStore is not InMemoryBackingStore inMemoryBackingStore) + if(backingStore is not InMemoryBackingStore inMemoryBackingStore) return default; - + var subscriptionsFieldInfo = typeof(InMemoryBackingStore).GetField("subscriptions", BindingFlags.NonPublic | BindingFlags.Instance); return (IDictionary>)subscriptionsFieldInfo?.GetValue(inMemoryBackingStore); } diff --git a/src/ApiClientBuilder.cs b/src/ApiClientBuilder.cs index 566280b0..f83b02f2 100644 --- a/src/ApiClientBuilder.cs +++ b/src/ApiClientBuilder.cs @@ -71,7 +71,7 @@ public static IParseNodeFactory EnableBackingStoreForParseNodeFactory(IParseNode } else result = new BackingStoreParseNodeFactory(original); - + return result; } private static void EnableBackingStoreForParseNodeRegistry(ParseNodeFactoryRegistry registry) diff --git a/src/ApiException.cs b/src/ApiException.cs index b464c5f2..334d1053 100644 --- a/src/ApiException.cs +++ b/src/ApiException.cs @@ -12,9 +12,9 @@ namespace Microsoft.Kiota.Abstractions; public class ApiException : Exception { /// - public ApiException(): base() + public ApiException() : base() { - + } /// public ApiException(string message) : base(message) @@ -24,7 +24,7 @@ public ApiException(string message) : base(message) public ApiException(string message, Exception innerException) : base(message, innerException) { } - + /// /// The HTTP response status code. /// diff --git a/src/BaseRequestBuilder.cs b/src/BaseRequestBuilder.cs index 910fec8d..ef16ed08 100644 --- a/src/BaseRequestBuilder.cs +++ b/src/BaseRequestBuilder.cs @@ -39,11 +39,11 @@ protected BaseRequestBuilder(IRequestAdapter requestAdapter, string urlTemplate, /// The request adapter to use to execute the requests. /// Url template to use to build the URL for the current request builder /// The raw URL to use for the current request builder - protected BaseRequestBuilder(IRequestAdapter requestAdapter, string urlTemplate, string rawUrl):this(requestAdapter, urlTemplate, new Dictionary() { + protected BaseRequestBuilder(IRequestAdapter requestAdapter, string urlTemplate, string rawUrl) : this(requestAdapter, urlTemplate, new Dictionary() { { RequestInformation.RawUrlKey, rawUrl } }) { - if (string.IsNullOrEmpty(rawUrl)) + if(string.IsNullOrEmpty(rawUrl)) { throw new ArgumentNullException(nameof(rawUrl)); } diff --git a/src/IResponseHandler.cs b/src/IResponseHandler.cs index 67aad082..cf3614b8 100644 --- a/src/IResponseHandler.cs +++ b/src/IResponseHandler.cs @@ -2,9 +2,9 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. // ------------------------------------------------------------------------------ -using System.Threading.Tasks; -using System.Collections.Generic; using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Kiota.Abstractions.Serialization; namespace Microsoft.Kiota.Abstractions diff --git a/src/Microsoft.Kiota.Abstractions.csproj b/src/Microsoft.Kiota.Abstractions.csproj index bdba8831..aea567dd 100644 --- a/src/Microsoft.Kiota.Abstractions.csproj +++ b/src/Microsoft.Kiota.Abstractions.csproj @@ -14,7 +14,7 @@ https://aka.ms/kiota/docs true true - 1.5.0 + 1.6.0 true false diff --git a/src/NativeResponseHandler.cs b/src/NativeResponseHandler.cs index f83d197d..5bb7a978 100644 --- a/src/NativeResponseHandler.cs +++ b/src/NativeResponseHandler.cs @@ -2,9 +2,9 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. // ------------------------------------------------------------------------------ -using System.Threading.Tasks; -using System.Collections.Generic; using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Kiota.Abstractions.Serialization; namespace Microsoft.Kiota.Abstractions diff --git a/src/QueryParameterAttribute.cs b/src/QueryParameterAttribute.cs index 1a369abb..12d57f4c 100644 --- a/src/QueryParameterAttribute.cs +++ b/src/QueryParameterAttribute.cs @@ -12,7 +12,7 @@ namespace Microsoft.Kiota.Abstractions; public sealed class QueryParameterAttribute : Attribute { private readonly string templateName; - + /// /// Creates a new instance of the attribute /// @@ -22,7 +22,7 @@ public QueryParameterAttribute(string templateName) if(string.IsNullOrEmpty(templateName)) throw new ArgumentNullException(nameof(templateName)); this.templateName = templateName; } - + /// /// The name of the parameter in the template. /// diff --git a/src/authentication/ApiKeyAuthenticationProvider.cs b/src/authentication/ApiKeyAuthenticationProvider.cs index 9378ca81..a0e507c7 100644 --- a/src/authentication/ApiKeyAuthenticationProvider.cs +++ b/src/authentication/ApiKeyAuthenticationProvider.cs @@ -30,11 +30,11 @@ public class ApiKeyAuthenticationProvider : IAuthenticationProvider /// The hosts that are allowed to use the provided API key. public ApiKeyAuthenticationProvider(string apiKey, string parameterName, KeyLocation keyLocation, params string[] allowedHosts) { - if (string.IsNullOrEmpty(apiKey)) + if(string.IsNullOrEmpty(apiKey)) throw new ArgumentNullException(nameof(apiKey)); - if (string.IsNullOrEmpty(parameterName)) + if(string.IsNullOrEmpty(parameterName)) throw new ArgumentNullException(nameof(parameterName)); - if (allowedHosts == null) + if(allowedHosts == null) throw new ArgumentNullException(nameof(allowedHosts)); ApiKey = apiKey; ParameterName = parameterName; @@ -45,21 +45,24 @@ public ApiKeyAuthenticationProvider(string apiKey, string parameterName, KeyLoca /// public Task AuthenticateRequestAsync(RequestInformation request, Dictionary? additionalAuthenticationContext = default, CancellationToken cancellationToken = default) { - if (request == null) + if(request == null) throw new ArgumentNullException(nameof(request)); using var span = _activitySource?.StartActivity(nameof(AuthenticateRequestAsync)); - if (!AllowedHostsValidator.IsUrlHostValid(request.URI)) { + if(!AllowedHostsValidator.IsUrlHostValid(request.URI)) + { span?.SetTag("com.microsoft.kiota.authentication.is_url_valid", false); return Task.CompletedTask; } var uri = request.URI; - if(!uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) { + if(!uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + { span?.SetTag("com.microsoft.kiota.authentication.is_url_valid", false); throw new ArgumentException("Only https is supported"); } - switch(KeyLoc) { + switch(KeyLoc) + { case KeyLocation.QueryParameter: var uriString = uri.OriginalString + (uri.Query != string.Empty ? "&" : "?") + $"{ParameterName}={ApiKey}"; request.URI = new Uri(uriString); @@ -76,7 +79,8 @@ public Task AuthenticateRequestAsync(RequestInformation request, Dictionary /// The location of the API key parameter. /// - public enum KeyLocation { + public enum KeyLocation + { /// /// The API key is passed as a query parameter. /// diff --git a/src/authentication/BaseBearerTokenAuthenticationProvider.cs b/src/authentication/BaseBearerTokenAuthenticationProvider.cs index ed9e6f58..bcc3e4dc 100644 --- a/src/authentication/BaseBearerTokenAuthenticationProvider.cs +++ b/src/authentication/BaseBearerTokenAuthenticationProvider.cs @@ -24,7 +24,7 @@ public BaseBearerTokenAuthenticationProvider(IAccessTokenProvider accessTokenPro /// /// Gets the to use for getting the access token. /// - public IAccessTokenProvider AccessTokenProvider {get; private set;} + public IAccessTokenProvider AccessTokenProvider { get; private set; } private const string AuthorizationHeaderKey = "Authorization"; private const string ClaimsKey = "claims"; diff --git a/src/extensions/IDictionaryExtensions.cs b/src/extensions/IDictionaryExtensions.cs index c7ca7e44..63e00180 100644 --- a/src/extensions/IDictionaryExtensions.cs +++ b/src/extensions/IDictionaryExtensions.cs @@ -28,7 +28,7 @@ public static bool TryAdd(this IDictionary dictionar { throw new ArgumentNullException(nameof(dictionary)); } - + if(key == null) { throw new ArgumentNullException(nameof(key)); diff --git a/src/serialization/KiotaJsonSerializer.Deserialization.cs b/src/serialization/KiotaJsonSerializer.Deserialization.cs new file mode 100644 index 00000000..27ac6817 --- /dev/null +++ b/src/serialization/KiotaJsonSerializer.Deserialization.cs @@ -0,0 +1,87 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.IO; +#if NET5_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Microsoft.Kiota.Abstractions.Serialization; + +/// +/// Set of helper methods for JSON serialization +/// +public static partial class KiotaJsonSerializer +{ + private const string _jsonContentType = "application/json"; + /// + /// Deserializes the given stream into an object. + /// + /// The factory to create the object. + /// The serialized representation of the object. + public static T? Deserialize(string serializedRepresentation, ParsableFactory parsableFactory) where T : IParsable + => KiotaSerializer.Deserialize(_jsonContentType, serializedRepresentation, parsableFactory); + /// + /// Deserializes the given stream into an object. + /// + /// The stream to deserialize. + /// The factory to create the object. + public static T? Deserialize(Stream stream, ParsableFactory parsableFactory) where T : IParsable + => KiotaSerializer.Deserialize(_jsonContentType, stream, parsableFactory); + /// + /// Deserializes the given stream into an object. + /// + /// The stream to deserialize. +#if NET5_0_OR_GREATER + public static T? Deserialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>(Stream stream) where T : IParsable +#else + public static T? Deserialize(Stream stream) where T : IParsable +#endif + => KiotaSerializer.Deserialize(_jsonContentType, stream); + /// + /// Deserializes the given stream into an object. + /// + /// The serialized representation of the object. +#if NET5_0_OR_GREATER + public static T? Deserialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>(string serializedRepresentation) where T : IParsable +#else + public static T? Deserialize(string serializedRepresentation) where T : IParsable +#endif + => KiotaSerializer.Deserialize(_jsonContentType, serializedRepresentation); + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The stream to deserialize. + /// The factory to create the object. + public static IEnumerable DeserializeCollection(Stream stream, ParsableFactory parsableFactory) where T : IParsable + => KiotaSerializer.DeserializeCollection(_jsonContentType, stream, parsableFactory); + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The serialized representation of the objects. + /// The factory to create the object. + public static IEnumerable DeserializeCollection(string serializedRepresentation, ParsableFactory parsableFactory) where T : IParsable + => KiotaSerializer.DeserializeCollection(_jsonContentType, serializedRepresentation, parsableFactory); + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The stream to deserialize. +#if NET5_0_OR_GREATER + public static IEnumerable DeserializeCollection<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>(Stream stream) where T : IParsable +#else + public static IEnumerable DeserializeCollection(Stream stream) where T : IParsable +#endif + => KiotaSerializer.DeserializeCollection(_jsonContentType, stream); + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The serialized representation of the object. +#if NET5_0_OR_GREATER + public static IEnumerable DeserializeCollection<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>(string serializedRepresentation) where T : IParsable +#else + public static IEnumerable DeserializeCollection(string serializedRepresentation) where T : IParsable +#endif + => KiotaSerializer.DeserializeCollection(_jsonContentType, serializedRepresentation); +} \ No newline at end of file diff --git a/src/serialization/KiotaJsonSerializer.Serialization.cs b/src/serialization/KiotaJsonSerializer.Serialization.cs new file mode 100644 index 00000000..aa87f420 --- /dev/null +++ b/src/serialization/KiotaJsonSerializer.Serialization.cs @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.IO; +#if NET5_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Microsoft.Kiota.Abstractions.Serialization; + +public static partial class KiotaJsonSerializer +{ + /// + /// Serializes the given object into a string based on the content type. + /// + /// Type of the object to serialize + /// The object to serialize. + /// The serialized representation as a stream. + public static Stream SerializeAsStream(T value) where T : IParsable + => KiotaSerializer.SerializeAsStream(_jsonContentType, value); + + /// + /// Serializes the given object into a string based on the content type. + /// + /// Type of the object to serialize + /// The object to serialize. + /// The serialized representation as a string. + public static string SerializeAsString(T value) where T : IParsable + => KiotaSerializer.SerializeAsString(_jsonContentType, value); + + /// + /// Serializes the given object into a string based on the content type. + /// + /// Type of the object to serialize + /// The object to serialize. + /// The serialized representation as a stream. + public static Stream SerializeAsStream(IEnumerable value) where T : IParsable + => KiotaSerializer.SerializeAsStream(_jsonContentType, value); + + /// + /// Serializes the given object into a string based on the content type. + /// + /// Type of the object to serialize + /// The object to serialize. + /// The serialized representation as a string. + public static string SerializeAsString(IEnumerable value) where T : IParsable + => KiotaSerializer.SerializeAsString(_jsonContentType, value); + +} \ No newline at end of file diff --git a/src/serialization/KiotaSerializer.Deserialization.cs b/src/serialization/KiotaSerializer.Deserialization.cs new file mode 100644 index 00000000..5756b08c --- /dev/null +++ b/src/serialization/KiotaSerializer.Deserialization.cs @@ -0,0 +1,135 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +#if NET5_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + + +namespace Microsoft.Kiota.Abstractions.Serialization; + +public static partial class KiotaSerializer +{ + /// + /// Deserializes the given stream into an object based on the content type. + /// + /// The content type of the stream. + /// The factory to create the object. + /// The serialized representation of the object. + public static T? Deserialize(string contentType, string serializedRepresentation, ParsableFactory parsableFactory) where T : IParsable + { + if(string.IsNullOrEmpty(serializedRepresentation)) throw new ArgumentNullException(nameof(serializedRepresentation)); + using var stream = GetStreamFromString(serializedRepresentation); + return Deserialize(contentType, stream, parsableFactory); + } + private static Stream GetStreamFromString(string source) + { + var stream = new MemoryStream(); + using var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true); + writer.WriteAsync(source).GetAwaiter().GetResult(); // so the asp.net projects don't get an error + writer.Flush(); + stream.Position = 0; + return stream; + } + /// + /// Deserializes the given stream into an object based on the content type. + /// + /// The content type of the stream. + /// The stream to deserialize. + /// The factory to create the object. + public static T? Deserialize(string contentType, Stream stream, ParsableFactory parsableFactory) where T : IParsable + { + if(string.IsNullOrEmpty(contentType)) throw new ArgumentNullException(nameof(contentType)); + if(stream == null) throw new ArgumentNullException(nameof(stream)); + if(parsableFactory == null) throw new ArgumentNullException(nameof(parsableFactory)); + var parseNode = ParseNodeFactoryRegistry.DefaultInstance.GetRootParseNode(contentType, stream); + return parseNode.GetObjectValue(parsableFactory); + } + /// + /// Deserializes the given stream into an object based on the content type. + /// + /// The content type of the stream. + /// The stream to deserialize. +#if NET5_0_OR_GREATER + public static T? Deserialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>(string contentType, Stream stream) where T : IParsable +#else + public static T? Deserialize(string contentType, Stream stream) where T : IParsable +#endif + => Deserialize(contentType, stream, GetFactoryFromType()); +#if NET5_0_OR_GREATER + private static ParsableFactory GetFactoryFromType<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>() where T : IParsable +#else + private static ParsableFactory GetFactoryFromType() where T : IParsable +#endif + { + var type = typeof(T); + var factoryMethod = Array.Find(type.GetMethods(), static x => x.IsStatic && "CreateFromDiscriminatorValue".Equals(x.Name, StringComparison.OrdinalIgnoreCase)) ?? + throw new InvalidOperationException($"No factory method found for type {type.Name}"); + return (ParsableFactory)factoryMethod.CreateDelegate(typeof(ParsableFactory)); + } + /// + /// Deserializes the given stream into an object based on the content type. + /// + /// The content type of the stream. + /// The serialized representation of the object. +#if NET5_0_OR_GREATER + public static T? Deserialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>(string contentType, string serializedRepresentation) where T : IParsable +#else + public static T? Deserialize(string contentType, string serializedRepresentation) where T : IParsable +#endif + => Deserialize(contentType, serializedRepresentation, GetFactoryFromType()); + + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The content type of the stream. + /// The stream to deserialize. + /// The factory to create the object. + public static IEnumerable DeserializeCollection(string contentType, Stream stream, ParsableFactory parsableFactory) where T : IParsable + { + if(string.IsNullOrEmpty(contentType)) throw new ArgumentNullException(nameof(contentType)); + if(stream == null) throw new ArgumentNullException(nameof(stream)); + if(parsableFactory == null) throw new ArgumentNullException(nameof(parsableFactory)); + var parseNode = ParseNodeFactoryRegistry.DefaultInstance.GetRootParseNode(contentType, stream); + return parseNode.GetCollectionOfObjectValues(parsableFactory); + } + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The content type of the stream. + /// The serialized representation of the objects. + /// The factory to create the object. + public static IEnumerable DeserializeCollection(string contentType, string serializedRepresentation, ParsableFactory parsableFactory) where T : IParsable + { + if(string.IsNullOrEmpty(serializedRepresentation)) throw new ArgumentNullException(nameof(serializedRepresentation)); + using var stream = GetStreamFromString(serializedRepresentation); + return DeserializeCollection(contentType, stream, parsableFactory); + } + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The content type of the stream. + /// The stream to deserialize. +#if NET5_0_OR_GREATER + public static IEnumerable DeserializeCollection<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>(string contentType, Stream stream) where T : IParsable +#else + public static IEnumerable DeserializeCollection(string contentType, Stream stream) where T : IParsable +#endif + => DeserializeCollection(contentType, stream, GetFactoryFromType()); + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The content type of the stream. + /// The serialized representation of the object. +#if NET5_0_OR_GREATER + public static IEnumerable DeserializeCollection<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>(string contentType, string serializedRepresentation) where T : IParsable +#else + public static IEnumerable DeserializeCollection(string contentType, string serializedRepresentation) where T : IParsable +#endif + => DeserializeCollection(contentType, serializedRepresentation, GetFactoryFromType()); +} \ No newline at end of file diff --git a/src/serialization/KiotaSerializer.Serialization.cs b/src/serialization/KiotaSerializer.Serialization.cs new file mode 100644 index 00000000..7abbd99b --- /dev/null +++ b/src/serialization/KiotaSerializer.Serialization.cs @@ -0,0 +1,81 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.IO; +#if NET5_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + + +namespace Microsoft.Kiota.Abstractions.Serialization; + +/// +/// Set of helper methods for serialization +/// +public static partial class KiotaSerializer +{ + /// + /// Serializes the given object into a string based on the content type. + /// + /// Type of the object to serialize + /// Content type to serialize the object to + /// The object to serialize. + /// The serialized representation as a stream. + public static Stream SerializeAsStream(string contentType, T value) where T : IParsable + { + using var writer = GetSerializationWriter(contentType, value); + writer.WriteObjectValue(string.Empty, value); + return writer.GetSerializedContent(); + } + /// + /// Serializes the given object into a string based on the content type. + /// + /// Type of the object to serialize + /// Content type to serialize the object to + /// The object to serialize. + /// The serialized representation as a string. + public static string SerializeAsString(string contentType, T value) where T : IParsable + { + using var stream = SerializeAsStream(contentType, value); + return GetStringFromStream(stream); + } + /// + /// Serializes the given object into a string based on the content type. + /// + /// Type of the object to serialize + /// Content type to serialize the object to + /// The object to serialize. + /// The serialized representation as a stream. + public static Stream SerializeAsStream(string contentType, IEnumerable value) where T : IParsable + { + using var writer = GetSerializationWriter(contentType, value); + writer.WriteCollectionOfObjectValues(string.Empty, value); + return writer.GetSerializedContent(); + } + /// + /// Serializes the given object into a string based on the content type. + /// + /// Type of the object to serialize + /// Content type to serialize the object to + /// The object to serialize. + /// The serialized representation as a string. + public static string SerializeAsString(string contentType, IEnumerable value) where T : IParsable + { + using var stream = SerializeAsStream(contentType, value); + return GetStringFromStream(stream); + } + private static string GetStringFromStream(Stream stream) + { + using var reader = new StreamReader(stream); + return reader.ReadToEndAsync().ConfigureAwait(false).GetAwaiter().GetResult(); // so the asp.net projects don't get an error + } + private static ISerializationWriter GetSerializationWriter(string contentType, object value) + { + if(string.IsNullOrEmpty(contentType)) throw new ArgumentNullException(nameof(contentType)); + if(value == null) throw new ArgumentNullException(nameof(value)); + return SerializationWriterFactoryRegistry.DefaultInstance.GetSerializationWriter(contentType); + } +} \ No newline at end of file diff --git a/src/serialization/ParseNodeFactoryRegistry.cs b/src/serialization/ParseNodeFactoryRegistry.cs index 8a02c1ca..231b5d2f 100644 --- a/src/serialization/ParseNodeFactoryRegistry.cs +++ b/src/serialization/ParseNodeFactoryRegistry.cs @@ -53,7 +53,7 @@ public IParseNode GetRootParseNode(string contentType, Stream content) var cleanedContentType = contentTypeVendorCleanupRegex.Replace(vendorSpecificContentType, string.Empty); if(ContentTypeAssociatedFactories.ContainsKey(cleanedContentType)) return ContentTypeAssociatedFactories[cleanedContentType].GetRootParseNode(cleanedContentType, content); - + throw new InvalidOperationException($"Content type {cleanedContentType} does not have a factory registered to be parsed"); } } diff --git a/src/serialization/ParseNodeHelper.cs b/src/serialization/ParseNodeHelper.cs index 9859795f..e6bb0415 100644 --- a/src/serialization/ParseNodeHelper.cs +++ b/src/serialization/ParseNodeHelper.cs @@ -11,13 +11,15 @@ namespace Microsoft.Kiota.Abstractions.Serialization; /// /// Helper methods for intersection wrappers /// -public static class ParseNodeHelper { +public static class ParseNodeHelper +{ /// /// Merges the given fields deserializers for an intersection type into a single collection. /// /// The collection of deserializers to merge. - public static IDictionary> MergeDeserializersForIntersectionWrapper(params IParsable?[] targets) { - if (targets == null) + public static IDictionary> MergeDeserializersForIntersectionWrapper(params IParsable?[] targets) + { + if(targets == null) { throw new ArgumentNullException(nameof(targets)); } @@ -25,7 +27,7 @@ public static IDictionary> MergeDeserializersForInter { throw new ArgumentException("At least one target must be provided.", nameof(targets)); } - + return targets.Where(static x => x != null) .SelectMany(static x => x!.GetFieldDeserializers()) .GroupBy(static x => x.Key) diff --git a/src/serialization/SerializationWriterFactoryRegistry.cs b/src/serialization/SerializationWriterFactoryRegistry.cs index 43a7a799..6ba7bce5 100644 --- a/src/serialization/SerializationWriterFactoryRegistry.cs +++ b/src/serialization/SerializationWriterFactoryRegistry.cs @@ -44,7 +44,7 @@ public ISerializationWriter GetSerializationWriter(string contentType) var vendorSpecificContentType = contentType.Split(";".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).First(); if(ContentTypeAssociatedFactories.ContainsKey(vendorSpecificContentType)) return ContentTypeAssociatedFactories[vendorSpecificContentType].GetSerializationWriter(vendorSpecificContentType); - + var cleanedContentType = ParseNodeFactoryRegistry.contentTypeVendorCleanupRegex.Replace(vendorSpecificContentType, string.Empty); if(ContentTypeAssociatedFactories.ContainsKey(cleanedContentType)) return ContentTypeAssociatedFactories[cleanedContentType].GetSerializationWriter(cleanedContentType); diff --git a/src/store/InMemoryBackingStore.cs b/src/store/InMemoryBackingStore.cs index 152b3f6d..adc5c187 100644 --- a/src/store/InMemoryBackingStore.cs +++ b/src/store/InMemoryBackingStore.cs @@ -74,7 +74,7 @@ public void Set(string key, T? value) { backedModel.BackingStore.InitializationCompleted = false;// All its properties are dirty as the model has been touched. Set(key, value); - },key); // use property name(key) as subscriptionId to prevent excess subscription creation in the event this is called again + }, key); // use property name(key) as subscriptionId to prevent excess subscription creation in the event this is called again } // if its an IBackedModel collection property to the store, subscribe to item properties' BackingStores and use the events to flag the collection property is "dirty" if(value is ICollection collectionValues) @@ -86,7 +86,7 @@ public void Set(string key, T? value) model.BackingStore.Subscribe((keyString, oldObject, newObject) => { Set(key, value); - },key);// use property name(key) as subscriptionId to prevent excess subscription creation in the event this is called again + }, key);// use property name(key) as subscriptionId to prevent excess subscription creation in the event this is called again }); } @@ -101,7 +101,7 @@ public void Set(string key, T? value) public IEnumerable> Enumerate() { if(ReturnOnlyChangedValues)// refresh the state of collection properties if they've changed in size. - store.ToList().ForEach(x => EnsureCollectionPropertyIsConsistent(x.Key,x.Value.Item2)); + store.ToList().ForEach(x => EnsureCollectionPropertyIsConsistent(x.Key, x.Value.Item2)); return (ReturnOnlyChangedValues ? store.Where(x => x.Value.Item1) : store) .Select(x => new KeyValuePair(x.Key, x.Value.Item2)); @@ -140,7 +140,7 @@ public void Subscribe(Action callback, string subscrip throw new ArgumentNullException(nameof(subscriptionId)); if(callback == null) throw new ArgumentNullException(nameof(callback)); - subscriptions.AddOrUpdate(subscriptionId, callback, (_,_) => callback); + subscriptions.AddOrUpdate(subscriptionId, callback, (_, _) => callback); } /// @@ -184,7 +184,7 @@ private void EnsureCollectionPropertyIsConsistent(string key, object? storeItem) // Call Get<>() on nested properties so that this method may be called recursively to ensure collections are consistent collectionTuple.Item1.OfType().ToList().ForEach(store => store.BackingStore.Enumerate() .ToList().ForEach(item => store.BackingStore.Get(item.Key))); - + if(collectionTuple.Item2 != collectionTuple.Item1.Count) // and the size has changed since we last updated) { Set(key, collectionTuple.Item1); //ensure the store is notified the collection property is "dirty"