diff --git a/RestSharp.Tests/RestSharp.Tests.csproj b/RestSharp.Tests/RestSharp.Tests.csproj index 2dc5e58c7..2bfe13443 100644 --- a/RestSharp.Tests/RestSharp.Tests.csproj +++ b/RestSharp.Tests/RestSharp.Tests.csproj @@ -80,6 +80,8 @@ + + @@ -103,7 +105,7 @@ - + diff --git a/RestSharp.Tests/SampleClasses/GoogleWeatherWithAttributes.cs b/RestSharp.Tests/SampleClasses/GoogleWeatherWithAttributes.cs new file mode 100644 index 000000000..1b0fc889b --- /dev/null +++ b/RestSharp.Tests/SampleClasses/GoogleWeatherWithAttributes.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using RestSharp.Deserializers; + +namespace RestSharp.Tests.SampleClasses +{ + public class GoogleWeatherApi + { + public string Version { get; set; } + public GoogleWeather Weather { get; set; } + } + + public class GoogleWeather : List + { + public string ModuleId { get; set; } + public string TabId { get; set; } + public string MobileRow { get; set; } + public string MobileZipped { get; set; } + public string Row { get; set; } + public string Section { get; set; } + + [DeserializeAs(Name = "forecast_information")] + public ForecastInformation Forecast { get; set; } + + [DeserializeAs(Name = "current_conditions")] + public CurrentConditions Current { get; set; } + } + + public class GoogleDataElement + { + public string Data { get; set; } + } + + public class ForecastInformation + { + public GoogleDataElement City { get; set; } + public GoogleDataElement PostalCode { get; set; } + public GoogleDataElement ForecastDate { get; set; } + public GoogleDataElement UnitSystem { get; set; } + } + + public class CurrentConditions + { + public GoogleDataElement Condition { get; set; } + public GoogleDataElement TempC { get; set; } + public GoogleDataElement Humidity { get; set; } + public GoogleDataElement Icon { get; set; } + public GoogleDataElement WindCondition { get; set; } + } + + public class ForecastConditions + { + public GoogleDataElement DayOfWeek { get; set; } + public GoogleDataElement Condition { get; set; } + public GoogleDataElement Low { get; set; } + public GoogleDataElement High { get; set; } + public GoogleDataElement Icon { get; set; } + } + + +} diff --git a/RestSharp.Tests/XmlAttributeDeserializerTests.cs b/RestSharp.Tests/XmlAttributeDeserializerTests.cs new file mode 100644 index 000000000..0cfc0e26b --- /dev/null +++ b/RestSharp.Tests/XmlAttributeDeserializerTests.cs @@ -0,0 +1,958 @@ +#region License +// Copyright 2010 John Sheehan +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#endregion + +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using RestSharp.Deserializers; +using Xunit; +using RestSharp.Tests.SampleClasses; +using System.Collections.Generic; + +namespace RestSharp.Tests +{ + public class XmlAttributeDeserializerTests + { + private const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5"; + private string SampleDataPath = Path.Combine(Environment.CurrentDirectory, "SampleData"); + + private string PathFor(string sampleFile) + { + return Path.Combine(SampleDataPath, sampleFile); + } + + [Fact] + public void Can_Deserialize_Lists_of_Simple_Types() + { + var xmlpath = PathFor("xmllists.xml"); + var doc = XDocument.Load(xmlpath); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new RestResponse() { Content = doc.ToString() }); + + Assert.NotEmpty(output.Names); + Assert.NotEmpty(output.Numbers); + Assert.False(output.Names[0].Length == 0); + Assert.False(output.Numbers.Sum() == 0); + } + + [Fact] + public void Can_Deserialize_To_List_Inheritor_From_Custom_Root_With_Attributes() + { + var xmlpath = PathFor("ListWithAttributes.xml"); + var doc = XDocument.Load(xmlpath); + + var xml = new XmlAttributeDeserializer(); + xml.RootElement = "Calls"; + var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); + + Assert.Equal(3, output.NumPages); + Assert.NotEmpty(output); + Assert.Equal(2, output.Count); + } + + [Fact] + public void Can_Deserialize_To_Standalone_List_Without_Matching_Class_Case() + { + var xmlpath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlpath); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize>(new RestResponse { Content = doc.ToString() }); + + Assert.NotEmpty(output); + Assert.Equal(4, output.Count); + } + + [Fact] + public void Can_Deserialize_To_Standalone_List_With_Matching_Class_Case() + { + var xmlpath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlpath); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize>(new RestResponse { Content = doc.ToString() }); + + Assert.NotEmpty(output); + Assert.Equal(4, output.Count); + } + + [Fact] + public void Can_Deserialize_Directly_To_Lists_Off_Root_Element() + { + var xmlpath = PathFor("directlists.xml"); + var doc = XDocument.Load(xmlpath); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize>(new RestResponse { Content = doc.ToString() }); + + Assert.NotEmpty(output); + Assert.Equal(2, output.Count); + } + + [Fact] + public void Can_Deserialize_Parentless_aka_Inline_List_Items_Without_Matching_Class_Name() + { + var xmlpath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlpath); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); + + Assert.NotEmpty(output.Images); + Assert.Equal(4, output.Images.Count); + } + + [Fact] + public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name() + { + var xmlpath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlpath); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); + + Assert.NotEmpty(output.images); + Assert.Equal(4, output.images.Count); + } + + [Fact] + public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name_With_Additional_Property() + { + var xmlpath = PathFor("InlineListSample.xml"); + var doc = XDocument.Load(xmlpath); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); + + Assert.Equal(4, output.Count); + } + + [Fact] + public void Can_Deserialize_Nested_List_Items_Without_Matching_Class_Name() + { + var xmlpath = PathFor("NestedListSample.xml"); + var doc = XDocument.Load(xmlpath); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); + + Assert.NotEmpty(output.Images); + Assert.Equal(4, output.Images.Count); + } + + + [Fact] + public void Can_Deserialize_Nested_List_Items_With_Matching_Class_Name() + { + var xmlpath = PathFor("NestedListSample.xml"); + var doc = XDocument.Load(xmlpath); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); + + Assert.NotEmpty(output.images); + Assert.Equal(4, output.images.Count); + } + + [Fact] + public void Can_Deserialize_Nested_List_Without_Elements_To_Empty_List() + { + var doc = CreateXmlWithEmptyNestedList(); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new RestResponse { Content = doc }); + + Assert.NotNull(output.images); + Assert.NotNull(output.Images); + Assert.Empty(output.images); + Assert.Empty(output.Images); + } + + [Fact] + public void Can_Deserialize_Inline_List_Without_Elements_To_Empty_List() + { + var doc = CreateXmlWithEmptyInlineList(); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new RestResponse { Content = doc }); + + Assert.NotNull(output.images); + Assert.NotNull(output.Images); + Assert.Empty(output.images); + Assert.Empty(output.Images); + } + + [Fact] + public void Can_Deserialize_Empty_Elements_to_Nullable_Values() + { + var doc = CreateXmlWithNullValues(); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new RestResponse { Content = doc }); + + Assert.Null(output.Id); + Assert.Null(output.StartDate); + Assert.Null(output.UniqueId); + } + + [Fact] + public void Can_Deserialize_Elements_to_Nullable_Values() + { + var culture = CultureInfo.InvariantCulture; + var doc = CreateXmlWithoutEmptyValues(culture); + var xml = new XmlAttributeDeserializer() {Culture = culture}; + var output = xml.Deserialize(new RestResponse { Content = doc }); + + Assert.NotNull(output.Id); + Assert.NotNull(output.StartDate); + Assert.NotNull(output.UniqueId); + + Assert.Equal(123, output.Id); + Assert.Equal(new DateTime(2010, 2, 21, 9, 35, 00), output.StartDate); + Assert.Equal(new Guid(GuidString), output.UniqueId); + } + + [Fact] + public void Can_Deserialize_TimeSpan() + { + var culture = CultureInfo.InvariantCulture; + var doc = new XDocument(culture); + + TimeSpan? nullTimespan = null; + TimeSpan? nullValueTimeSpan = new TimeSpan(21, 30, 7); + + var root = new XElement("Person"); + root.Add(new XElement("Tick", new TimeSpan(468006))); + root.Add(new XElement("Millisecond", new TimeSpan(0, 0, 0, 0, 125))); + root.Add(new XElement("Second", new TimeSpan(0, 0, 8))); + root.Add(new XElement("Minute", new TimeSpan(0, 55, 2))); + root.Add(new XElement("Hour", new TimeSpan(21, 30, 7))); + root.Add(new XElement("NullableWithoutValue", nullTimespan)); + root.Add(new XElement("NullableWithValue", nullValueTimeSpan)); + + doc.Add(root); + + var response = new RestResponse { Content = doc.ToString() }; + + var d = new XmlAttributeDeserializer() + { + Culture = culture, + }; + var payload = d.Deserialize(response); + Assert.Equal(new TimeSpan(468006), payload.Tick); + Assert.Equal(new TimeSpan(0, 0, 0, 0, 125), payload.Millisecond); + Assert.Equal(new TimeSpan(0, 0, 8), payload.Second); + Assert.Equal(new TimeSpan(0, 55, 2), payload.Minute); + Assert.Equal(new TimeSpan(21, 30, 7), payload.Hour); + Assert.Null(payload.NullableWithoutValue); + Assert.NotNull(payload.NullableWithValue); + Assert.Equal(new TimeSpan(21, 30, 7), payload.NullableWithValue.Value); + } + + [Fact] + public void Can_Deserialize_Custom_Formatted_Date() + { + var culture = CultureInfo.InvariantCulture; + var format = "dd yyyy MMM, hh:mm ss tt zzz"; + var date = new DateTime(2010, 2, 8, 11, 11, 11); + + var doc = new XDocument(); + + var root = new XElement("Person"); + root.Add(new XElement("StartDate", date.ToString(format, culture))); + + doc.Add(root); + + var xml = new XmlAttributeDeserializer + { + DateFormat = format, + Culture = culture + }; + + var response = new RestResponse { Content = doc.ToString() }; + var output = xml.Deserialize(response); + + Assert.Equal(date, output.StartDate); + } + + [Fact] + public void Can_Deserialize_Elements_On_Default_Root() + { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.Equal(false, p.IsCool); + + Assert.Equal(new Guid(GuidString), p.UniqueId); + Assert.Equal(Guid.Empty, p.EmptyGuid); + + Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + + Assert.Equal(Order.Third, p.Order); + Assert.Equal(Disposition.SoSo, p.Disposition); + + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + } + + [Fact] + public void Can_Deserialize_Attributes_On_Default_Root() + { + var doc = CreateAttributesXml(); + var response = new RestResponse { Content = doc }; + + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.Equal(false, p.IsCool); + Assert.Equal(new Guid(GuidString), p.UniqueId); + Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + } + + [Fact] + public void Ignore_Protected_Property_That_Exists_In_Data() + { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Null(p.IgnoreProxy); + } + + [Fact] + public void Ignore_ReadOnly_Property_That_Exists_In_Data() + { + var doc = CreateElementsXml(); + var response = new RestResponse { Content = doc }; + + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Null(p.ReadOnlyProxy); + } + + [Fact] + public void Can_Deserialize_Names_With_Underscores_On_Default_Root() + { + var doc = CreateUnderscoresXml(); + var response = new RestResponse { Content = doc }; + + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.Equal(false, p.IsCool); + Assert.Equal(new Guid(GuidString), p.UniqueId); + Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + + Assert.NotNull(p.Foes); + Assert.Equal(5, p.Foes.Count); + Assert.Equal("Yankees", p.Foes.Team); + } + + + [Fact] + public void Can_Deserialize_Names_With_Dashes_On_Default_Root() + { + var doc = CreateDashesXml(); + var response = new RestResponse { Content = doc }; + + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.Equal(false, p.IsCool); + Assert.Equal(new Guid(GuidString), p.UniqueId); + Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + + Assert.NotNull(p.Foes); + Assert.Equal(5, p.Foes.Count); + Assert.Equal("Yankees", p.Foes.Team); + } + + [Fact] + public void Can_Deserialize_Names_With_Underscores_Without_Matching_Case_On_Default_Root () + { + var doc = CreateLowercaseUnderscoresXml (); + var response = new RestResponse { Content = doc }; + + var d = new XmlAttributeDeserializer (); + var p = d.Deserialize (response); + + Assert.Equal ("John Sheehan", p.Name); + Assert.Equal (new DateTime (2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal (28, p.Age); + Assert.Equal (long.MaxValue, p.BigNumber); + Assert.Equal (99.9999m, p.Percent); + Assert.Equal (false, p.IsCool); + Assert.Equal (new Guid (GuidString), p.UniqueId); + Assert.Equal (new Uri ("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal (new Uri ("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + + Assert.NotNull (p.Friends); + Assert.Equal (10, p.Friends.Count); + + Assert.NotNull (p.BestFriend); + Assert.Equal ("The Fonz", p.BestFriend.Name); + Assert.Equal (1952, p.BestFriend.Since); + + Assert.NotNull (p.Foes); + Assert.Equal (5, p.Foes.Count); + Assert.Equal ("Yankees", p.Foes.Team); + } + + [Fact] + public void Can_Deserialize_Lower_Cased_Root_Elements_With_Dashes() + { + var doc = CreateDashesXml(); + var response = new RestResponse { Content = doc }; + + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize(response); + + Assert.Equal("John Sheehan", p.Name); + Assert.Equal(new DateTime(2009, 9, 25, 0, 6, 1), p.StartDate); + Assert.Equal(28, p.Age); + Assert.Equal(long.MaxValue, p.BigNumber); + Assert.Equal(99.9999m, p.Percent); + Assert.Equal(false, p.IsCool); + Assert.Equal(new Guid(GuidString), p.UniqueId); + Assert.Equal(new Uri("http://example.com", UriKind.RelativeOrAbsolute), p.Url); + Assert.Equal(new Uri("/foo/bar", UriKind.RelativeOrAbsolute), p.UrlPath); + + Assert.NotNull(p.Friends); + Assert.Equal(10, p.Friends.Count); + + Assert.NotNull(p.BestFriend); + Assert.Equal("The Fonz", p.BestFriend.Name); + Assert.Equal(1952, p.BestFriend.Since); + + Assert.NotNull(p.Foes); + Assert.Equal(5, p.Foes.Count); + Assert.Equal("Yankees", p.Foes.Team); + } + + [Fact] + public void Can_Deserialize_Root_Elements_Without_Matching_Case_And_Dashes() + { + var doc = CreateLowerCasedRootElementWithDashesXml(); + var response = new RestResponse { Content = doc }; + + var d = new XmlAttributeDeserializer(); + var p = d.Deserialize>(response); + + Assert.NotNull(p); + Assert.Equal(1, p.Count); + Assert.Equal(45, p[0].ConceptId); + } + + + [Fact] + public void Can_Deserialize_Eventful_Xml() + { + var xmlpath = PathFor("eventful.xml"); + var doc = XDocument.Load(xmlpath); + var response = new RestResponse { Content = doc.ToString() }; + + var d = new XmlAttributeDeserializer(); + var output = d.Deserialize(response); + + Assert.NotEmpty(output.venues); + Assert.Equal(3, output.venues.Count); + Assert.Equal("Tivoli", output.venues[0].name); + Assert.Equal("http://eventful.com/brisbane/venues/tivoli-/V0-001-002169294-8", output.venues[1].url); + Assert.Equal("V0-001-000266914-3", output.venues[2].id); + } + + [Fact] + public void Can_Deserialize_Lastfm_Xml() + { + var xmlpath = PathFor("Lastfm.xml"); + var doc = XDocument.Load(xmlpath); + var response = new RestResponse { Content = doc.ToString() }; + + var d = new XmlAttributeDeserializer(); + var output = d.Deserialize(response); + + //Assert.NotEmpty(output.artists); + Assert.Equal("http://www.last.fm/event/328799+Philip+Glass+at+Barbican+Centre+on+12+June+2008", output.url); + Assert.Equal("http://www.last.fm/venue/8777860+Barbican+Centre", output.venue.url); + } + + [Fact] + public void Can_Deserialize_Google_Weather_Xml() + { + var xmlpath = PathFor("GoogleWeather.xml"); + var doc = XDocument.Load(xmlpath); + var response = new RestResponse { Content = doc.ToString() }; + + var d = new XmlAttributeDeserializer(); + var output = d.Deserialize(response); + + Assert.NotEmpty(output.weather); + Assert.Equal(4, output.weather.Count); + Assert.Equal("Sunny", output.weather[0].condition.data); + } + + [Fact] + public void Can_Deserialize_Google_Weather_Xml_WithDeserializeAs() + { + var xmlpath = PathFor("GoogleWeather.xml"); + var doc = XDocument.Load(xmlpath); + var response = new RestResponse { Content = doc.ToString() }; + + var d = new XmlAttributeDeserializer(); + var output = d.Deserialize(response); + + Assert.NotEmpty(output.Weather); + Assert.Equal(4, output.Weather.Count); + Assert.Equal("Sunny", output.Weather[0].Condition.Data); + } + + [Fact] + public void Can_Deserialize_Boolean_From_Number() + { + var xmlpath = PathFor("boolean_from_number.xml"); + var doc = XDocument.Load(xmlpath); + var response = new RestResponse { Content = doc.ToString() }; + + var d = new XmlAttributeDeserializer(); + var output = d.Deserialize(response); + + Assert.True(output.Value); + } + + [Fact] + public void Can_Deserialize_Boolean_From_String() + { + var xmlpath = PathFor("boolean_from_string.xml"); + var doc = XDocument.Load(xmlpath); + var response = new RestResponse { Content = doc.ToString() }; + + var d = new XmlAttributeDeserializer(); + var output = d.Deserialize(response); + + Assert.True(output.Value); + } + + [Fact] + public void Can_Deserialize_Empty_Elements_With_Attributes_to_Nullable_Values() + { + var doc = CreateXmlWithAttributesAndNullValues(); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new RestResponse {Content = doc}); + + Assert.Null(output.Id); + Assert.Null(output.StartDate); + Assert.Null(output.UniqueId); + } + + [Fact] + public void Can_Deserialize_Mixture_Of_Empty_Elements_With_Attributes_And_Populated_Elements() + { + var doc = CreateXmlWithAttributesAndNullValuesAndPopulatedValues(); + + var xml = new XmlAttributeDeserializer(); + var output = xml.Deserialize(new RestResponse {Content = doc}); + + Assert.Null(output.Id); + Assert.Null(output.StartDate); + Assert.Equal(new Guid(GuidString), output.UniqueId); + } + + [Fact] + public void Can_Deserialize_DateTimeOffset() + { + var culture = CultureInfo.InvariantCulture; + var doc = new XDocument(culture); + + DateTimeOffset DateTimeOffset = new DateTimeOffset(2013, 02, 08, 9, 18, 22, TimeSpan.FromHours(10)); + DateTimeOffset? NullableDateTimeOffsetWithValue = new DateTimeOffset(2013, 02, 08, 9, 18, 23, TimeSpan.FromHours(10)); + + var root = new XElement("Dates"); + root.Add(new XElement("DateTimeOffset", DateTimeOffset)); + root.Add(new XElement("NullableDateTimeOffsetWithNull", string.Empty)); + root.Add(new XElement("NullableDateTimeOffsetWithValue", NullableDateTimeOffsetWithValue)); + + doc.Add(root); + + var xml = new XmlAttributeDeserializer + { + Culture = culture, + }; + + var response = new RestResponse { Content = doc.ToString() }; + + var d = new XmlAttributeDeserializer() + { + Culture = culture, + }; + var payload = d.Deserialize(response); + Assert.Equal(DateTimeOffset, payload.DateTimeOffset); + Assert.Null(payload.NullableDateTimeOffsetWithNull); + + Assert.True(payload.NullableDateTimeOffsetWithValue.HasValue); + Assert.Equal(NullableDateTimeOffsetWithValue, payload.NullableDateTimeOffsetWithValue); + } + + private static string CreateUnderscoresXml() + { + var doc = new XDocument(); + var root = new XElement("Person"); + root.Add(new XElement("Name", "John Sheehan")); + root.Add(new XElement("Start_Date", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute("Age", 28)); + root.Add(new XElement("Percent", 99.9999m)); + root.Add(new XElement("Big_Number", long.MaxValue)); + root.Add(new XAttribute("Is_Cool", false)); + root.Add(new XElement("Ignore", "dummy")); + root.Add(new XAttribute("Read_Only", "dummy")); + root.Add(new XElement("Unique_Id", new Guid(GuidString))); + root.Add(new XElement("Url", "http://example.com")); + root.Add(new XElement("Url_Path", "/foo/bar")); + + root.Add(new XElement("Best_Friend", + new XElement("Name", "The Fonz"), + new XAttribute("Since", 1952) + )); + + var friends = new XElement("Friends"); + for (int i = 0; i < 10; i++) + { + friends.Add(new XElement("Friend", + new XElement("Name", "Friend" + i), + new XAttribute("Since", DateTime.Now.Year - i) + )); + } + root.Add(friends); + + var foes = new XElement("Foes"); + foes.Add(new XAttribute("Team", "Yankees")); + for (int i = 0; i < 5; i++) + { + foes.Add(new XElement("Foe", new XElement("Nickname", "Foe" + i))); + } + root.Add(foes); + + doc.Add(root); + return doc.ToString(); + } + + private static string CreateLowercaseUnderscoresXml() + { + var doc = new XDocument(); + var root = new XElement("Person"); + root.Add(new XElement("Name", "John Sheehan")); + root.Add(new XElement("start_date", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute("Age", 28)); + root.Add(new XElement("Percent", 99.9999m)); + root.Add(new XElement("big_number", long.MaxValue)); + root.Add(new XAttribute("is_cool", false)); + root.Add(new XElement("Ignore", "dummy")); + root.Add(new XAttribute("read_only", "dummy")); + root.Add(new XElement("unique_id", new Guid(GuidString))); + root.Add(new XElement("Url", "http://example.com")); + root.Add(new XElement("url_path", "/foo/bar")); + + root.Add(new XElement("best_friend", + new XElement("name", "The Fonz"), + new XAttribute("Since", 1952) + )); + + var friends = new XElement("Friends"); + for (int i = 0; i < 10; i++) + { + friends.Add(new XElement("Friend", + new XElement("Name", "Friend" + i), + new XAttribute("Since", DateTime.Now.Year - i) + )); + } + root.Add(friends); + + var foes = new XElement("Foes"); + foes.Add(new XAttribute("Team", "Yankees")); + for (int i = 0; i < 5; i++) + { + foes.Add(new XElement("Foe", new XElement("Nickname", "Foe" + i))); + } + root.Add(foes); + + doc.Add(root); + + return doc.ToString(); + } + + private static string CreateDashesXml() + { + var doc = new XDocument(); + var root = new XElement("Person"); + root.Add(new XElement("Name", "John Sheehan")); + root.Add(new XElement("Start_Date", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute("Age", 28)); + root.Add(new XElement("Percent", 99.9999m)); + root.Add(new XElement("Big-Number", long.MaxValue)); + root.Add(new XAttribute("Is-Cool", false)); + root.Add(new XElement("Ignore", "dummy")); + root.Add(new XAttribute("Read-Only", "dummy")); + root.Add(new XElement("Unique-Id", new Guid(GuidString))); + root.Add(new XElement("Url", "http://example.com")); + root.Add(new XElement("Url-Path", "/foo/bar")); + + root.Add(new XElement("Best-Friend", + new XElement("Name", "The Fonz"), + new XAttribute("Since", 1952) + )); + + var friends = new XElement("Friends"); + for (int i = 0; i < 10; i++) + { + friends.Add(new XElement("Friend", + new XElement("Name", "Friend" + i), + new XAttribute("Since", DateTime.Now.Year - i) + )); + } + root.Add(friends); + + var foes = new XElement("Foes"); + foes.Add(new XAttribute("Team", "Yankees")); + for (int i = 0; i < 5; i++) + { + foes.Add(new XElement("Foe", new XElement("Nickname", "Foe" + i))); + } + root.Add(foes); + + doc.Add(root); + return doc.ToString(); + } + + private static string CreateLowerCasedRootElementWithDashesXml() + { + var doc = new XDocument(); + var root = new XElement("incoming-invoices", + new XElement("incoming-invoice", + new XElement("concept-id", 45) + ) + ); + doc.Add(root); + return doc.ToString(); + } + + private static string CreateElementsXml() + { + var doc = new XDocument(); + var root = new XElement("Person"); + root.Add(new XElement("Name", "John Sheehan")); + root.Add(new XElement("StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XElement("Age", 28)); + root.Add(new XElement("Percent", 99.9999m)); + root.Add(new XElement("BigNumber", long.MaxValue)); + root.Add(new XElement("IsCool", false)); + root.Add(new XElement("Ignore", "dummy")); + root.Add(new XElement("ReadOnly", "dummy")); + + root.Add(new XElement("UniqueId", new Guid(GuidString))); + root.Add(new XElement("EmptyGuid", "")); + + root.Add(new XElement("Url", "http://example.com")); + root.Add(new XElement("UrlPath", "/foo/bar")); + root.Add(new XElement("Order", "third")); + root.Add(new XElement("Disposition", "so-so")); + + root.Add(new XElement("BestFriend", + new XElement("Name", "The Fonz"), + new XElement("Since", 1952) + )); + + var friends = new XElement("Friends"); + for (int i = 0; i < 10; i++) + { + friends.Add(new XElement("Friend", + new XElement("Name", "Friend" + i), + new XElement("Since", DateTime.Now.Year - i) + )); + } + root.Add(friends); + + doc.Add(root); + return doc.ToString(); + } + + private static string CreateAttributesXml() + { + var doc = new XDocument(); + var root = new XElement("Person"); + root.Add(new XAttribute("Name", "John Sheehan")); + root.Add(new XAttribute("StartDate", new DateTime(2009, 9, 25, 0, 6, 1))); + root.Add(new XAttribute("Age", 28)); + root.Add(new XAttribute("Percent", 99.9999m)); + root.Add(new XAttribute("BigNumber", long.MaxValue)); + root.Add(new XAttribute("IsCool", false)); + root.Add(new XAttribute("Ignore", "dummy")); + root.Add(new XAttribute("ReadOnly", "dummy")); + root.Add(new XAttribute("UniqueId", new Guid(GuidString))); + root.Add(new XAttribute("Url", "http://example.com")); + root.Add(new XAttribute("UrlPath", "/foo/bar")); + + root.Add(new XElement("BestFriend", + new XAttribute("Name", "The Fonz"), + new XAttribute("Since", 1952) + )); + + doc.Add(root); + return doc.ToString(); + } + + private static string CreateXmlWithNullValues() + { + var doc = new XDocument(); + var root = new XElement("NullableValues"); + + root.Add(new XElement("Id", null), + new XElement("StartDate", null), + new XElement("UniqueId", null) + ); + + doc.Add(root); + + return doc.ToString(); + } + + private static string CreateXmlWithoutEmptyValues(CultureInfo culture) + { + var doc = new XDocument(); + var root = new XElement("NullableValues"); + + root.Add(new XElement("Id", 123), + new XElement("StartDate", new DateTime(2010, 2, 21, 9, 35, 00).ToString(culture)), + new XElement("UniqueId", new Guid(GuidString)) + ); + + doc.Add(root); + + return doc.ToString(); + } + + private static string CreateXmlWithEmptyNestedList() + { + var doc = new XDocument(); + var root = new XElement("EmptyListSample"); + + root.Add(new XElement("Images")); + + doc.Add(root); + + return doc.ToString(); + } + + private static string CreateXmlWithEmptyInlineList() + { + var doc = new XDocument(); + var root = new XElement("EmptyListSample"); + + doc.Add(root); + + return doc.ToString(); + } + + private static string CreateXmlWithAttributesAndNullValues() + { + var doc = new XDocument(); + var root = new XElement("NullableValues"); + + var idElement = new XElement("Id", null); + idElement.SetAttributeValue("SomeAttribute", "SomeAttribute_Value"); + root.Add(idElement, + new XElement("StartDate", null), + new XElement("UniqueId", null) + ); + + doc.Add(root); + + return doc.ToString(); + } + + private static string CreateXmlWithAttributesAndNullValuesAndPopulatedValues() + { + var doc = new XDocument(); + var root = new XElement("NullableValues"); + + var idElement = new XElement("Id", null); + idElement.SetAttributeValue("SomeAttribute", "SomeAttribute_Value"); + root.Add(idElement, + new XElement("StartDate", null), + new XElement("UniqueId", new Guid(GuidString)) + ); + + doc.Add(root); + + return doc.ToString(); + } + + } +} diff --git a/RestSharp.Tests/XmlTests.cs b/RestSharp.Tests/XmlDeserializerTests.cs similarity index 96% rename from RestSharp.Tests/XmlTests.cs rename to RestSharp.Tests/XmlDeserializerTests.cs index 29abaf5b7..6deef898d 100644 --- a/RestSharp.Tests/XmlTests.cs +++ b/RestSharp.Tests/XmlDeserializerTests.cs @@ -26,7 +26,7 @@ namespace RestSharp.Tests { - public class XmlTests + public class XmlDeserializerTests { private const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5"; private string SampleDataPath = Path.Combine(Environment.CurrentDirectory, "SampleData"); @@ -118,19 +118,6 @@ public void Can_Deserialize_Parentless_aka_Inline_List_Items_Without_Matching_Cl Assert.Equal(4, output.Images.Count); } - [Fact] - public void Can_Deserialize_Parentless_aka_Inline_List_Items_Without_Matching_Class_Name_Using_XmlAttributeDeserializer() - { - var xmlpath = PathFor("InlineListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output.Images); - Assert.Equal(4, output.Images.Count); - } - [Fact] public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name() { @@ -144,19 +131,6 @@ public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class Assert.Equal(4, output.images.Count); } - [Fact] - public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name_Using_XmlAttributeDeserializer() - { - var xmlpath = PathFor("InlineListSample.xml"); - var doc = XDocument.Load(xmlpath); - - var xml = new XmlAttributeDeserializer(); - var output = xml.Deserialize(new RestResponse { Content = doc.ToString() }); - - Assert.NotEmpty(output.images); - Assert.Equal(4, output.images.Count); - } - [Fact] public void Can_Deserialize_Parentless_aka_Inline_List_Items_With_Matching_Class_Name_With_Additional_Property() { @@ -274,11 +248,6 @@ public void Can_Deserialize_TimeSpan() doc.Add(root); - var xml = new XmlDeserializer - { - Culture = culture, - }; - var response = new RestResponse { Content = doc.ToString() }; var d = new XmlDeserializer() diff --git a/RestSharp/Deserializers/XmlAttributeDeserializer.cs b/RestSharp/Deserializers/XmlAttributeDeserializer.cs index 2cf8a7d6f..aea2af8ea 100644 --- a/RestSharp/Deserializers/XmlAttributeDeserializer.cs +++ b/RestSharp/Deserializers/XmlAttributeDeserializer.cs @@ -14,377 +14,36 @@ // limitations under the License. #endregion -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Xml; +using System.Reflection; using System.Xml.Linq; -using RestSharp.Authenticators.OAuth.Extensions; using RestSharp.Extensions; namespace RestSharp.Deserializers { - public class XmlAttributeDeserializer : IDeserializer + public class XmlAttributeDeserializer : XmlDeserializer { - public string RootElement { get; set; } - public string Namespace { get; set; } - public string DateFormat { get; set; } - public CultureInfo Culture { get; set; } - - public XmlAttributeDeserializer() - { - Culture = CultureInfo.InvariantCulture; - } - - public T Deserialize(IRestResponse response) - { - if (response.Content == null) - return default(T); - - var doc = XDocument.Parse(response.Content); - var root = doc.Root; - if (RootElement.HasValue() && doc.Root != null) - { - root = doc.Root.Element(RootElement.AsNamespaced(Namespace)); - } - - // autodetect xml namespace - if (!Namespace.HasValue()) - { - RemoveNamespace(doc); - } - - var x = Activator.CreateInstance(); - var objType = x.GetType(); - - if (objType.IsSubclassOfRawGeneric(typeof(List<>))) - { - x = (T)HandleListDerivative(x, root, objType.Name, objType); - } - else - { - Map(x, root); - } - - return x; - } - - void RemoveNamespace(XDocument xdoc) - { - foreach (XElement e in xdoc.Root.DescendantsAndSelf()) - { - if (e.Name.Namespace != XNamespace.None) - { - e.Name = XNamespace.None.GetName(e.Name.LocalName); - } - if (e.Attributes().Any(a => a.IsNamespaceDeclaration || a.Name.Namespace != XNamespace.None)) - { - e.ReplaceAttributes(e.Attributes().Select(a => a.IsNamespaceDeclaration ? null : a.Name.Namespace != XNamespace.None ? new XAttribute(XNamespace.None.GetName(a.Name.LocalName), a.Value) : a)); - } - } - } - - private void Map(object x, XElement root) - { - var objType = x.GetType(); - var props = objType.GetProperties(); - - foreach (var prop in props) - { - var type = prop.PropertyType; - - if (!type.IsPublic || !prop.CanWrite) - continue; - - var name = prop.Name.AsNamespaced(Namespace); - var isAttribute = false; - //Check for the DeserializeAs attribute on the property - var options = prop.GetAttribute(); - if (options != null) - { - name = options.Name ?? name; - isAttribute = options.Attribute; - } - - var value = GetValueFromXml(root, name, isAttribute); - - var stringValue = value as string; - if (stringValue.IsNullOrBlank()) - { - // special case for inline list items - if (type.IsGenericType) - { - var genericType = type.GetGenericArguments()[0]; - - var first = GetElementByName(root, genericType.Name); - if (first != null) - { - var elements = root.Elements(first.Name); - - var list = (IList)Activator.CreateInstance(type); - PopulateListFromElements(genericType, elements, list); - prop.SetValue(x, list, null); - - } - } - continue; - } - - // check for nullable and extract underlying type - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - type = type.GetGenericArguments()[0]; - - if (string.IsNullOrEmpty(value.ToString())) - { - continue; - } - } - - if (type == typeof(bool)) - { - var toConvert = value.ToString().ToLower(); - prop.SetValue(x, XmlConvert.ToBoolean(toConvert), null); - } - else if (type.IsPrimitive) - { - prop.SetValue(x, value.ChangeType(type, Culture), null); - } - else if (type.IsEnum) - { - var converted = type.FindEnumValue(value.ToString(), Culture); - prop.SetValue(x, converted, null); - } - else if (type == typeof(Uri)) - { - var uri = new Uri(value.ToString(), UriKind.RelativeOrAbsolute); - prop.SetValue(x, uri, null); - } - else if (type == typeof(string)) - { - prop.SetValue(x, value, null); - } - else if (type == typeof(DateTime)) - { - if (DateFormat.HasValue()) - { - value = DateTime.ParseExact(value.ToString(), DateFormat, Culture); - } - else - { - value = DateTime.Parse(value.ToString(), Culture); - } - - prop.SetValue(x, value, null); - } - else if (type == typeof(Decimal)) - { - value = Decimal.Parse(value.ToString(), Culture); - prop.SetValue(x, value, null); - } - else if (type == typeof(Guid)) - { - value = new Guid(value.ToString()); - prop.SetValue(x, value, null); - } - else if (type.IsGenericType) - { - var t = type.GetGenericArguments()[0]; - var list = (IList)Activator.CreateInstance(type); - - var container = GetElementByName(root, name); - var first = container.Elements().FirstOrDefault(); - - var elements = container.Elements().Where(d => d.Name == first.Name); - PopulateListFromElements(t, elements, list); - - prop.SetValue(x, list, null); - } - else if (type.IsSubclassOfRawGeneric(typeof(List<>))) - { - // handles classes that derive from List - // e.g. a collection that also has properties - var list = HandleListDerivative(x, root, name.ToString(), type); - prop.SetValue(x, list, null); - } - else - { - // nested property classes - if (root != null) - { - var element = GetElementByName(root, name); - if (element != null) - { - var item = CreateAndMap(type, element); - prop.SetValue(x, item, null); - } - } - } - } - } - - private void PopulateListFromElements(Type t, IEnumerable elements, IList list) - { - foreach (var element in elements) - { - var item = CreateAndMap(t, element); - list.Add(item); - } - } - - private object HandleListDerivative(object x, XElement root, string propName, Type type) - { - var t = type.BaseType.GetGenericArguments()[0]; - - var name = t.Name; - //Gets the DeserialiseAs Attribute for the Class that the list uses - var options = t.GetAttribute(); - if (options != null) - { - name = options.Name ?? name; - } - - var lowerName = name.ToLower(); - var camelName = name.ToCamelCase(Culture); - - var list = (IList)Activator.CreateInstance(type); - - IEnumerable elements = null; - - if (root.Descendants(name.AsNamespaced(Namespace)).Count() != 0) - { - elements = root.Descendants(t.Name.AsNamespaced(Namespace)); - } - - if (root.Descendants(lowerName).Count() != 0) - { - elements = root.Descendants(lowerName); - } - - if (root.Descendants(camelName).Count() != 0) - { - elements = root.Descendants(camelName); - } - - PopulateListFromElements(t, elements, list); - - // get properties too, not just list items - Map(list, root.Element(propName.AsNamespaced(Namespace))); - - return list; - } - - private object CreateAndMap(Type t, XElement element) - { - var item = Activator.CreateInstance(t); - Map(item, element); - return item; - } - - private object GetValueFromXml(XElement root, XName name, bool attribute) - { - object val = null; - - if (root == null) return null; - - //check if the property is set as an Attribute using DeserializeAs - if (attribute) - { - var attributeVal = GetAttributeByName(root, name); - if (attributeVal != null) - { - val = attributeVal.Value; - } - } - else - { - //Not set as an attribute - var element = GetElementByName(root, name); - if (element == null) - { - var attributeVal = GetAttributeByName(root, name); - if (attributeVal != null) - { - val = attributeVal.Value; - } - } - else - { - if (!element.IsEmpty || element.HasElements || element.HasAttributes) - { - val = element.Value; - } - } - } - - return val; - } - - private XElement GetElementByName(XElement root, XName name) - { - var lowerName = XName.Get(name.LocalName.ToLower(), name.NamespaceName); - var camelName = XName.Get(name.LocalName.ToCamelCase(Culture), name.NamespaceName); - - if (root.Element(name) != null) - { - return root.Element(name); - } - - if (root.Element(lowerName) != null) - { - return root.Element(lowerName); - } - - if (root.Element(camelName) != null) - { - return root.Element(camelName); - } - - if (name == "Value" && root.Value != null) - { - return root; - } - - // try looking for element that matches sanitized property name (Order by depth) - var element = root.Descendants().OrderBy(d => d.Ancestors().Count()).FirstOrDefault(d => d.Name.LocalName.RemoveUnderscoresAndDashes() == name.LocalName); - if (element != null) - { - return element; - } - - return null; - } - - private XAttribute GetAttributeByName(XElement root, XName name) - { - var lowerName = XName.Get(name.LocalName.ToLower(), name.NamespaceName); - var camelName = XName.Get(name.LocalName.ToCamelCase(Culture), name.NamespaceName); - - if (root.Attribute(name) != null) - { - return root.Attribute(name); - } - - if (root.Attribute(lowerName) != null) - { - return root.Attribute(lowerName); - } - - if (root.Attribute(camelName) != null) - { - return root.Attribute(camelName); - } - - // try looking for element that matches sanitized property name - var element = root.Attributes().FirstOrDefault(d => d.Name.LocalName.RemoveUnderscoresAndDashes() == name.LocalName); - if (element != null) - { - return element; - } - - return null; - } + protected override object GetValueFromXml(XElement root, XName name, PropertyInfo prop) + { + var isAttribute = false; + + //Check for the DeserializeAs attribute on the property + var options = prop.GetAttribute(); + if (options != null) + { + name = options.Name ?? name; + isAttribute = options.Attribute; + } + + if (isAttribute) + { + var attributeVal = GetAttributeByName(root, name); + if (attributeVal != null) + { + return attributeVal.Value; + } + } + + return base.GetValueFromXml(root, name, prop); + } } } diff --git a/RestSharp/Deserializers/XmlDeserializer.cs b/RestSharp/Deserializers/XmlDeserializer.cs index 0d7200766..3d7e93cd0 100644 --- a/RestSharp/Deserializers/XmlDeserializer.cs +++ b/RestSharp/Deserializers/XmlDeserializer.cs @@ -18,6 +18,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Xml.Linq; using RestSharp.Extensions; @@ -39,7 +40,7 @@ public XmlDeserializer() Culture = CultureInfo.InvariantCulture; } - public T Deserialize(IRestResponse response) + public virtual T Deserialize(IRestResponse response) { if (string.IsNullOrEmpty( response.Content )) return default(T); @@ -72,7 +73,7 @@ public T Deserialize(IRestResponse response) return x; } - void RemoveNamespace(XDocument xdoc) + private void RemoveNamespace(XDocument xdoc) { foreach (XElement e in xdoc.Root.DescendantsAndSelf()) { @@ -87,7 +88,7 @@ void RemoveNamespace(XDocument xdoc) } } - private void Map(object x, XElement root) + protected virtual void Map(object x, XElement root) { var objType = x.GetType(); var props = objType.GetProperties(); @@ -100,7 +101,7 @@ private void Map(object x, XElement root) continue; var name = prop.Name.AsNamespaced(Namespace); - var value = GetValueFromXml(root, name); + var value = GetValueFromXml(root, name, prop); if (value == null) { @@ -342,7 +343,7 @@ private object HandleListDerivative(object x, XElement root, string propName, Ty return list; } - private object CreateAndMap(Type t, XElement element) + protected virtual object CreateAndMap(Type t, XElement element) { object item; if (t == typeof(String)) @@ -362,7 +363,7 @@ private object CreateAndMap(Type t, XElement element) return item; } - private object GetValueFromXml(XElement root, XName name) + protected virtual object GetValueFromXml(XElement root, XName name, PropertyInfo prop) { object val = null; @@ -389,7 +390,7 @@ private object GetValueFromXml(XElement root, XName name) return val; } - private XElement GetElementByName(XElement root, XName name) + protected virtual XElement GetElementByName(XElement root, XName name) { var lowerName = name.LocalName.ToLower().AsNamespaced(name.NamespaceName); var camelName = name.LocalName.ToCamelCase(Culture).AsNamespaced(name.NamespaceName); @@ -430,7 +431,7 @@ private XElement GetElementByName(XElement root, XName name) return null; } - private XAttribute GetAttributeByName(XElement root, XName name) + protected virtual XAttribute GetAttributeByName(XElement root, XName name) { var lowerName = name.LocalName.ToLower().AsNamespaced(name.NamespaceName); var camelName = name.LocalName.ToCamelCase(Culture).AsNamespaced(name.NamespaceName);