I am writing a class library which abstracts data contained in XML files on a web site. Each XML file uses the same root element: page. The descendant(s) of page depend on the specific file that I am downloading. For example:
<!-- http://.../groups.xml -->
<page url="/groups.xml">
<groups>
<group id="1" >
<members>
<member name="Adrian" />
<member name="Sophie" />
<member name="Roger" />
</members>
</group>
</groups>
</page>
<!-- http://.../project.xml?n=World%20Domination -->
<page url="/project.xml">
<projectInfo>
<summary classified="true" deadline="soon" />
<team>
<member name="Pat" />
<member name="George" />
</team>
</projectInfo>
</page>
There are also several additional XML files that I would like to download and process, eventually. For that reason, I have been trying to come up with a nice, clean way to deserialize the data. I've tried a few approaches, but each approach leaves me feeling a little dirty when I look back over my code. My latest incarnation utilizes the following method:
internal class Provider
{
/// <summary>Download information from the host.</summary>
/// <typeparam name="T">The type of data being downloaded.</typeparam>
internal T Download<T>(string url) where T : IXmlSerializable, new()
{
try
{
var request = (HttpWebRequest)WebRequest.Create(url);
var response = (HttpWebResponse)request.GetResponse();
using (var reader = XmlReader.Create(response.GetResponseStream()))
{
// Skip the XML prolog (declaration and stylesheet reference).
reader.MoveToContent();
// Skip the `page` element.
if (reader.LocalName == "page") reader.ReadStartElement();
var serializer = new XmlSerializer(typeof(T));
return (T)serializer.Deserialize(reader);
}
}
catch (WebException ex) { /* The tubes are clogged. */ }
}
}
[XmlRoot(TypeName = "groups")]
public class GroupList : List<Group>, IXmlSerializable
{
private List<Group> _list;
public void ReadXml(XmlReader reader)
{
if (_list == null) _list = new List<Group>();
reader.ReadToDescendant("group");
do
{
var id = (int)reader["id"];
var group = new Group(id);
if (reader.ReadToDescendant("member"))
{
do
{
var member = new Member(reader["name"], group);
group.Add(member);
} while (reader.ReadToNextSibling("member"));
}
_list.Add(group);
} while (reader.ReadToNextSibling("group"));
reader.Read();
}
}
This works, but I feel like there is a better way that I'm not seeing. I tried using the xsd.exe utility when I started this project. While it would minimize the amount of code for me to write, it did not feel like the ideal solution. It would be the same approach that I'm using now -- I would just get there faster. I'm looking for a better solution. All pages have the page element in common -- isn't there a way to take advantage of that? Would it be possible to have a serializable container class Page that could contain a combination of other objects depending on the file downloaded? Are there any simpler ways to accomplish this?
.NET provides a "xsd.exe" utility on the command line.
Run xsd.exe (xmlfilename) on your original xml file and it'll derive a XML schema (xsd) from your XML data file.
Run xsd.exe (xsd file name) /C and it'll create a C# class which can be used to deserialize such an XML file into a C# class.
Of course, since it only has a single XML file to go on, xsd.exe isn't perfect in its XML schema it derives - but that could be quick'n'easy starting point for you to get started.
Related
I have this file for my settings.xml
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!--Settings document for System Gazatter-->
<Settings>
<OutputState>2</OutputState>
<SystemPrefix>XCL</SystemPrefix>
<MaxNumSystems>2147483647</MaxNumSystems>
<OpenCluster>0</OpenCluster>
<GardenWorld>False</GardenWorld>
<StellarVariance>False</StellarVariance>
<StellarMassOverride allow="True">
<MinStellarMass>1.00</MinStellarMass>
<MaxStellarMass>2</MaxStellarMass>
</StellarMassOverride>
<NumberOfStarsOverride allow="False">1</NumberOfStarsOverride>
</Settings>
I want to write a function that is something like
public static T getSettingElementValue(XDocument settings, string elementName) {
return (T)(settingDoc.Element("Settings")
.Select(x=>x.Element(elementName))
.First()
.Value);
}
Now, I know this won't work. (It's also missing any checks on calls.) Is there some way to do this? Or should I fall back to something like.
public static string getSettingElementStringValue(...)
public static string getSettingElementDoubleValue(...)
This works for me:
public static T getSettingElementValue<T>(XDocument settings, string elementName) {
return (T)Convert.ChangeType(settings.Element("Settings").Element(elementName).Value, typeof(T));
}
void Main()
{
var xml = XDocument.Load(#"C:\abc\blah.xml");
Console.WriteLine(getSettingElementValue<bool>(xml, "GardenWorld"));
}
...but I would still follow the advice of not having your data like this and just using the App.config or something easier. This'll also only work if T implements IConvertible, so you should probably add that type constraint on T.
You already have an answer, but I wrote an XML library (See here) a while back that does this in it with the following:
XElement settings = XElement.Load(file); // or .Parse(string)
bool flag = settings.Get("GardenWorld", false); // false is a default and does the generic type for you
One of the ways it does it is similar to the answer, but it also uses other methods if they don't work, such as trying for a TryParse(string) on the type and checking to see if the type has a string constructor.
Is there a way to debug XSLT documents that are loaded from a database by a custom XmlUrlResolver or does anyone know, what the errormessage below is about?
I have a XSLT stylesheet that imports a common xslt document:
<xsl:import href="db://common.hist.org"/>
The Scheme is handled by a custom XmlResolver that loads the XSLT document from a DB, but I get an error:
An entry with the same key already exists.
The common XSLT document referred to by the xsl:import contains some common XSLT templates, each with a unique name.
This error began to occur after having moved the XSLT documents from the local file system to the database. When using default import schemes pointing to local files and when loading the XSLT documents from the local filesystem, the error does not occur.
I also tried to turn on debugging when creating the instance of the XslCompiledTransform, but somehow it is not possible to "step into" the database-based XSLT.
_xslHtmlOutput = new XslCompiledTransform(XSLT_DEBUG);
Update: The following is basically the resolver code as requested, but the exception is not happening inside my code; thus I guess no obvious reason in this code below. (This same code is actually used to load the XSLT stylesheets that contain the imports, and when commenting out the imports everything works as expected.)
public class XmlDBResolver : XmlUrlResolver
{
private IDictionary<string,string> GetUriComponents(String uri)
{
bool useXmlPre = false;
uri = uri.Replace("db://", "");
useXmlPre = uri.StartsWith("xml/");
uri = uri.Replace("xml/", "");
IDictionary<string, string> dict = new Dictionary<string, string>();
string app = null, area = null, subArea = null;
if (!String.IsNullOrWhiteSpace(uri))
{
string[] components = uri.Split('.');
if (components == null)
throw new Exception("Invalid Xslt URI");
switch (components.Count())
{
case 3:
app = components[0];
break;
case 4:
area = components[0];
app = components[1];
break;
case 5:
subArea = components[0];
area = components[1];
app = components[2];
break;
default:
throw new Exception("Invalid Xslt URI");
}
dict.Add("application", app);
dict.Add("area", area);
dict.Add("subArea", subArea);
dict.Add("xmlPreTransform", String.Format("{0}", useXmlPre));
}
return dict;
}
public override System.Net.ICredentials Credentials
{
set { /* TODO: check if we need credentials */ }
}
public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
{
/*
* db://<app>.hist.org
* db://<area>.<app>.hist.org
* db://<subArea>.<area>.<app>.hist.org
*
* */
Tracing.TraceHelper.WriteLine(String.Format("GetEntity {0}", absoluteUri));
XmlReader reader = null;
switch (absoluteUri.Scheme)
{
case "db":
string origString = absoluteUri.OriginalString;
IDictionary<string, string> xsltDict = GetUriComponents(origString);
if(String.IsNullOrWhiteSpace(xsltDict["area"]))
{
reader = DatabaseServiceFactory.DatabaseService.GetApplicationXslt(xsltDict["application"]);
}
else if (!String.IsNullOrWhiteSpace(xsltDict["area"]) && String.IsNullOrWhiteSpace(xsltDict["subArea"]) && !Boolean.Parse(xsltDict["xmlPreTransform"]))
{
reader = DatabaseServiceFactory.DatabaseService.GetAreaXslt(xsltDict["application"], xsltDict["area"]);
}
else if (!String.IsNullOrWhiteSpace(xsltDict["area"]) && !String.IsNullOrWhiteSpace(xsltDict["subArea"]))
{
if(Boolean.Parse(xsltDict["xmlPreTransform"]))
reader = DatabaseServiceFactory.DatabaseService.GetSubareaXmlPreTransformXslt(xsltDict["application"], xsltDict["area"], xsltDict["subArea"]);
else
reader = DatabaseServiceFactory.DatabaseService.GetSubareaXslt(xsltDict["application"], xsltDict["area"], xsltDict["subArea"]);
}
return reader;
default:
return base.GetEntity(absoluteUri, role, ofObjectToReturn);
}
}
and for completeness the IDatabaseService interface (relevant parts):
public interface IDatabaseService
{
...
XmlReader GetApplicationXslt(String applicationName);
XmlReader GetAreaXslt(String applicationName, String areaName);
XmlReader GetSubareaXslt(String applicationName, String areaName, String subAreaName);
XmlReader GetSubareaXmlPreTransformXslt(String applicationName, String areaName, String subAreaName);
}
Update: I tried to isolate the problem by temporarily loading the stylesheets from a web server instead, which works. I learned that the SQL Server apparently stores only XML fragments without the XML declaration, in contrast to the stylesheets being stored on a webserver.
Update: The stacktrace of the Exception:
System.Xml.Xsl.XslLoadException: XSLT-Kompilierungsfehler. Fehler bei (9,1616). ---> System.ArgumentException: An entry with the same key already exists.. bei System.Collections.Specialized.ListDictionary.Add(Object key, Object value) bei System.Collections.Specialized.HybridDictionary.Add(Object key, Object value) bei System.Xml.Xsl.Xslt.XsltLoader.LoadStylesheet(XmlReader reader, Boolean include) bei System.Xml.Xsl.Xslt.XsltLoader.LoadStylesheet(Uri uri, Boolean include) bei System.Xml.Xsl.Xslt.XsltLoader.LoadStylesheet(XmlReader reader, Boolean include) --- Ende der inneren Ablaufverfolgung des Ausnahmestacks --- bei System.Xml.Xsl.Xslt.XsltLoader.LoadStylesheet(XmlReader reader, Boolean include) bei System.Xml.Xsl.Xslt.XsltLoader.Load(XmlReader reader) bei System.Xml.Xsl.Xslt.XsltLoader.Load(Compiler compiler, Object stylesheet, XmlResolver xmlResolver) bei System.Xml.Xsl.Xslt.Compiler.Compile(Object stylesheet, XmlResolver xmlResolver, QilExpression& qil) bei System.Xml.Xsl.XslCompiledTransform.LoadInternal(Object stylesheet, XsltSettings settings, XmlResolver stylesheetResolver) bei System.Xml.Xsl.XslCompiledTransform.Load(String stylesheetUri, XsltSettings settings, XmlResolver stylesheetResolver) bei (my namespace and class).GetXslTransform(Boolean preTransform) bei (my namespace and class).get_XslHtmlOutput() bei (my namespace and class).get_DisplayMarkup()
Short answer:
Your IDatabaseService interface methods return XmlReader objects. When you construct these, make sure to pass a baseUri to the constructor; e.g.:
public XmlReader GetApplicationXslt(string applicationName)
{
…
var baseUri = string.Format("db://{0}.hist.org", applicationName);
return XmlReader.Create(input: …,
settings: …,
baseUri: baseUri); // <-- this one is important!
}
If you specify this parameter, everything just might work fine. See the last section of this answer to see why I am suggesting this.
Long answer, introduction: Possible error sources:
Let's first briefly think about what component(s) could cause the error:
"This error began to occur after having moved the XSLT documents from the local file system to the database. When using default import schemes pointing to local files and when loading the XSLT documents from the local filesystem, the error does not occur."
Putting stylesheets in the database means that you must have…
changed the import paths in the stylesheets (introduced db://… paths)
implemented and hooked up a custom XmlDbResolver for handling the db:// import scheme
implemented database access code in the form of IDatabaseService, which backs XmlDbResolver
If the stylesheets are unchanged except for the import paths, it would seem likely that the error is either in your XmlResolver class and/or in IDatabaseService implementation. Since you haven't shown the code for the latter, we cannot debug your code without some guessing.
I have created a mock project using your XmlDbResolver (a full description follows below). I could not reproduce the error, thus I suspect that your IDatabaseService implementation causes the error.
Update: I have been able to reproduce the error. See the OP's comment & the last section of this answer.
My attempt to reproduce your error:
I've created a console application project in Visual Studio 2010 (which you can retrieve by cloning this Gist using Git (git clone https://gist.github.com/fbbd5e7319bd6c281c50b4ebb1cee1f9.git) and then checking out the 2nd commit, git checkout d00629). I'll describe each of the solution's items in more detail below.
(Note that the Copy to output directory property of SqlServerDatabase.mdf, TestInput.xml, and of both .xslt project items should be set to Always.)
SqlServerDatabase.mdf:
This is a service-based database which I'll be attaching to a local instance of SQL Server Express 2008. (This is done via the connection string in App.config; see below.)
I've set up the following items inside this database:
This table contains two columns which are defined as follows:
The tables are initially empty. Test data will be added to the database at runtime (see Program.cs and CommonHistOrg.xslt below).
App.config:
This file contains a connection string entry for the above database.
<?xml version="1.0"?>
<configuration>
<connectionStrings>
<add name="SqlServerDatabase"
connectionString="Data Source=.\SQLEXPRESS;
AttachDbFilename=|DataDirectory|\SqlServerDatabase.mdf;
Integrated Security=True;
User Instance=True"
/>
</connectionStrings>
</configuration>
IDatabaseService.cs:
This file contains the definition for your IDatabaseService interface, which I'm not repeating here.
SqlServerDatabaseService.cs:
This contains a class that implements IDatabaseService. It reads/writes data to the above database:
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.IO;
using System.Xml;
class SqlServerDatabaseService : IDatabaseService
{
// creates a connection based on connection string from App.config:
SqlConnection CreateConnection()
{
return new SqlConnection(connectionString: ConfigurationManager.ConnectionStrings["SqlServerDatabase"].ConnectionString);
}
// stores an XML document into the 'ApplicationDocuments' table:
public void StoreApplicationDocument(string applicationName, XmlReader document)
{
using (var connection = CreateConnection())
{
SqlCommand command = connection.CreateCommand();
command.CommandText = "INSERT INTO ApplicationDocuments (ApplicationName, Document) VALUES (#applicationName, #document)";
command.Parameters.Add(new SqlParameter("#applicationName", applicationName));
command.Parameters.Add(new SqlParameter("#document", new SqlXml(document)));
// ^^^^^^^^^^^^^^^^^^^^
connection.Open();
int numberOfRowsInserted = command.ExecuteNonQuery();
connection.Close();
}
}
// reads an XML document from the 'ApplicationDocuments' table:
public XmlReader GetApplicationXslt(string applicationName)
{
using (var connection = CreateConnection())
{
SqlCommand command = connection.CreateCommand();
command.CommandText = "SELECT Document FROM ApplicationDocuments WHERE ApplicationName = #applicationName";
command.Parameters.Add(new SqlParameter("#applicationName", applicationName));
connection.Open();
var plainXml = (string)command.ExecuteScalar();
connection.Close();
if (plainXml != null)
{
return XmlReader.Create(new StringReader(plainXml));
}
else
{
throw new KeyNotFoundException(message: string.Format("Database does not contain a application document named '{0}'.", applicationName));
}
}
}
… // (all other methods throw a NotImplementedException)
}
XmlDbResolver.cs:
This contains the XmlDbResolver class, which is identical to your XmlDBResolver class except for two changes:
The public constructor accepts an IDatabaseService object. This is used instead of DatabaseServiceFactory.DatabaseService.
I've had to remove the call to Tracing.TraceHelper.WriteLine.
CommonHistOrg.xslt:
This is the db://common.hist.org stylesheet, which will be put into the database at runtime (see Program.cs below):
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="Foo">
<Bar/>
</xsl:template>
</xsl:stylesheet>
TestStylesheet.xml:
This is a stylesheet which references the above db://common.hist.org stylesheet:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:import href="db://common.hist.org"/>
</xsl:stylesheet>
TestInput.xml:
This is the XML test input document that we're going to transform using the above TestStylesheet.xslt:
<?xml version="1.0" encoding="utf-8" ?>
<Foo/>
Program.cs:
This contains the test application code:
using System;
using System.Text;
using System.Xml;
using System.Xml.Xsl;
class Program
{
static void Main(string[] args)
{
var databaseService = new SqlServerDatabaseService();
// put CommonHistOrg.xslt into the 'ApplicationDocuments' database table:
databaseService.StoreApplicationDocument(
applicationName: "common",
document: XmlReader.Create("CommonHistOrg.xslt"));
// load the XSLT stylesheet:
var xslt = new XslCompiledTransform();
xslt.Load(#"TestStylesheet.xslt",
settings: XsltSettings.Default,
stylesheetResolver: new XmlDbResolver(databaseService));
// load the XML test input:
var input = XmlReader.Create("TestInput.xml");
// transform the test input and store the result in 'output':
var output = new StringBuilder();
xslt.Transform(input, XmlWriter.Create(output));
// display the transformed output:
Console.WriteLine(output.ToString());
Console.ReadLine();
}
}
Works like a charm on my machine: The output is an XML document with an empty root element <Bar/>, which is what the db://common.hist.org stylesheet outputs for the matched <Foo/> element from the test input.
Update: Error reproduction & fix:
Insert the following statement in the Main method:
databaseService.StoreApplicationDocument(
applicationName: "test",
document: XmlReader.Create("TestStylesheet.xslt"));
Instead of
xslt.Load(#"TestStylesheet.xslt", …);
do
xslt.Load(#"db://test.hist.org", …);
This triggers the error reported by the OP.
After some debugging, I have found out that the following does not cause this problem.
The fact that the Document column in the database table has type XML. It fails with NTEXT, too.
The fact that the <?xml … ?> header is missing from the documents that are returned from the DB. The error persists even when the XML header is manually added back before SqlServerDatabaseService returns control to the framework.
In fact, the error is triggered somewhere in the .NET Framework code. Which is why I decided to download and install the .NET Framework reference source. (I changed the solution to use version 3.5 of the framework for debugging purposes.) Installing this and restarting VS then allows you to see and step through the framework code during a debugging session.
Starting at the call to xslt.Load(…;) in our Main method, I stepped into the framework code and eventually came to a method LoadStylesheet inside XsltLoader.cs. There's a HybridDictionary called documentUrisInUse, which apparently stores base URIs of already-loaded stylesheets. So if we load more than one stylesheet with an empty or missing base URI, this method will try to add null to that dictionary twice; and this is what causes the error.
So once you assign a unique base URI to each stylesheet returned by your IDatabaseService, everything should work fine. You do this by passing a baseUri to the XmlReader constructor. See a code example at the very beginning of my answer. You can also retrieve an updated, working solution by downloading or cloning this Gist (git clone https://gist.github.com/fbbd5e7319bd6c281c50b4ebb1cee1f9.git).
I'm writing a file reader using the XmlReader in a Silverlight project. However, I'm getting some errors (specifically around the XmlReader.ReadStartElement method) and it's causing me to believe that I've misunderstood how to use it somewhere along the way.
Basically, here is a sample of the format of the Xml I am using:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<root>
<EmptyElement />
<NonEmptyElement Name="NonEmptyElement">
<SubElement Name="SubElement" />
</NonEmptyElement>
</root>
And here is a sample of some code used in the same way as how I am using it:
public void ReadData(XmlReader reader)
{
// Move to root element
reader.ReadStartElement("root");
// Move to the empty element
reader.ReadStartElement("EmptyElement");
// Read any children
while(reader.ReadToNextSibling("SubEmptyElement"))
{
// ...
}
// Read the end of the empty element
reader.ReadEndElement();
// Move to the non empty element
reader.ReadStartElement("NonEmptyElement"); // NOTE: This is where I get the error.
// ...
}
So, essentially, I am simply trying to read each element and any contained children. The error I get at the highlighted point is as follows:
Error Description
[Xml_InvalidNodeType]
Arguments: None,10,8
Debugging resource strings are unavailable. Often the key and arguments provide sufficient information to diagnose the problem. See http://go.microsoft.com/fwlink/?linkid=106663&Version=4.0.51204.0&File=System.Xml.dll&Key=Xml_InvalidNodeType
Error Stack Trace
at System.Xml.XmlReader.ReadStartElement(String name)
at ----------------
Any advice or direction on this would be greatly appreciated.
EDIT
Since this reader needs to be fairly generic, it can be assumed that the Xml may contain elements that are children of the EmptyElement. As such, the attempt at reading any SubEmptyElements should be valid.
<SubElement/> is not a sibling of <EmptyElement>, so <NonEmptyElement> is going to get skipped entirely, and your call to ReadEndElement() will read the end element </root>. When you try to subsequently read "NonEmptyElement", there are no elements left, and you'll get an XmlException: {"'None' is an invalid XmlNodeType. Line 8, position 1."}
Note also that since <EmptyElement/> is empty, when you ReadStartElement("EmptyElement"), you'll read the whole element, and you won't need to use ReadEndElement().
I'd also recommend that you configure your reader settings to IgnoreWhitespace (if you're not already doing so), to avoid any complications introduced by reading (insignificant) whitespace text nodes when you aren't expecting them.
Try moving the Read of NonEmptyElement up:
public static void ReadData(XmlReader reader)
{
reader.ReadStartElement("root");
reader.ReadStartElement("EmptyElement");
reader.ReadStartElement("NonEmptyElement");
while (reader.ReadToNextSibling("SubEmptyElement"))
{
// ...
}
reader.ReadEndElement(/* NonEmptyElement */);
reader.ReadEndElement(/* root */);
// ...
}
If you just want to skip anything in <EmptyElement>, regardless of whether or not its actually empty, use ReadToFollowing:
public static void ReadData(XmlReader reader)
{
reader.ReadStartElement("root");
reader.ReadToFollowing("NonEmptyElement");
Console.WriteLine(reader.GetAttribute("Name"));
reader.ReadStartElement("NonEmptyElement");
Console.WriteLine(reader.GetAttribute("Name"));
while (reader.ReadToNextSibling("SubEmptyElement"))
{
// ...
}
reader.ReadEndElement(/* NonEmptyElement */);
reader.ReadEndElement(/* root */);
// ...
}
Update: Here's a fuller example with a clearer data model. Maybe this is closer to what you're asking for.
XMLFile1.xml:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<root>
<Person Type="Homeless"/>
<Person Type="Developer">
<Home Type="Apartment" />
</Person>
<Person Type="Banker">
<Home Type="Apartment"/>
<Home Type="Detached"/>
<Home Type="Mansion">
<PoolHouse/>
</Home>
</Person>
</root>
Program.cs:
using System;
using System.Xml;
namespace ConsoleApplication6
{
internal class Program
{
public static void ReadData(XmlReader reader)
{
reader.ReadStartElement("root");
while (reader.IsStartElement("Person"))
{
ReadPerson(reader);
}
reader.ReadEndElement( /* root */);
}
public static void ReadPerson(XmlReader reader)
{
Console.WriteLine(reader.GetAttribute("Type"));
bool isEmpty = reader.IsEmptyElement;
reader.ReadStartElement("Person");
while (reader.IsStartElement("Home"))
{
ReadHome(reader);
}
if (!isEmpty)
{
reader.ReadEndElement( /* Person */);
}
}
public static void ReadHome(XmlReader reader)
{
Console.WriteLine("\t" + reader.GetAttribute("Type"));
bool isEmpty = reader.IsEmptyElement;
reader.ReadStartElement("Home");
if (!isEmpty)
{
reader.Skip();
reader.ReadEndElement( /* Home */);
}
}
private static void Main(string[] args)
{
var settings = new XmlReaderSettings { IgnoreWhitespace = true };
using (var xr = XmlReader.Create("XMLFile1.xml", settings))
{
ReadData(xr);
}
Console.ReadKey();
}
}
}
I have wsdl from third party server. Ran svcutil and ended up wih a set of
XmlNode AMethod(object Request);
methods. There is a separate 100 page pdf describing response/request objects for each method
My thought was wrap web methods and use XmlSerializer to return strongly typed objects. Returned xml looks like this (i removed soap headers):
<Response xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:type="ResponseExt"
xmlns="http://www.thirdparty.com/lr/">
<Code>0</Code>
<Message>SUCCESS</Message>
<SessionId>session_token</SessionId>
</Response>
Looked simple. Created a class(from document/wire captures):
[XmlRoot("Response")]
//EDIT added XmlType
[XmlType("ResponseExt", Namespace = "http://www.thirdparty.com/lr/")]
public class MyClass {
public string Code {get; set;}
public string Message {get; set;}
public string SessionId {get; set;}
}
Processing time:
//XmlNode node = xml from above
XmlSerializer serializer = new XmlSerializer(typeof(MyClass));
XmlNodeReader reader = new XmlNodeReader(node);
Myclass myclass = serializer.Deserialize(reader) as MyClass
Last line is where it blows up with inner exception message: The specified type was not recognized: name='ResponseExt', namespace='http://www.thirdparty.com/lr/', at <Response xmlns=''>.
I can't figure out how to make Serializer happy and what exactly these two mean
xsi:type="ResponseExt"
xmlns="http://www.thirdparty.com/lr/
As always any advice and pointer are appreciated
EDIT: Accepted answer below.
I was still getting exception, until i found this, hopefully it'll save someone some time.
I started to work backwards. Captured xml on the wire. Deserialized to my created classes with correct attributes: worked like a charm. Tried again from webservice - exception. For some reason XmlSerializer doesn't recognize ResponseExt.
XmlSerializer serializer = new XmlSerializer(typeof(Response));
XmlNode node = (XmlNode)results[0];
XmlDocument doc = new XmlDocument();
doc.LoadXml(node.OuterXml); //reload node
XmlNodeReader reader = new XmlNodeReader(doc.FirstChild); //there is only one node
Response rsp = serializer.Deserialize(reader) as Response; //works
EDIT: underlying issue wsdl file was not complete. After spending 2 days on this and finding this (ugly) workaround, third-party vendor provided complete WSDL with all types that deserialize without errors.
Why are you manually deserializing XML, when you have WSDL ?
If you have WSDL, use the svcutil.exe tool, or the wsdl.exe tool, to generate proxy classes and DTOs for the XML messages being sent and received on the wire.
The point of a web services toolkit, or "stack" is to provide this for you, so that you don't have to author classes and XML serialization code by hand.
Did you try this? Did you try to run the WSDL through one of those tools? Or did you try to "Add web reference" in Visual Studio?
After updating the question, I suggest that you modify the WSDL, rather than write custom code. You can produce a custom WSDL for the service, which will correctly generate the proxy classes you want. If you don't need all 100 methods (or however many there are), then leave them out. If you want a custom object from a method, then define a complexType that corresponds to that object. This is much simpler and more reliable than hand-authoring XML deserialization code for each method.
If you don't like that idea, and want to stick with manually writin the XML deserialization code, then you need to do two things:
attach a namespace to the XmlRoot attribute.
change the name of your class to ResponseExt, and derive it from a class called Response. Decorate that Response class with an XmlInclude attribute. This aligns the use of the Xml Serializer with the xsi:type used in the XML fragment.
It looks like this in code:
[XmlRoot("Response", Namespace="http://www.thirdparty.com/lr/")]
public class ResponseExt : Response {
}
[XmlRoot("Response", Namespace="http://www.thirdparty.com/lr/")]
[XmlInclude(typeof(ResponseExt))]
public class Response {
public string Code {get; set;}
public string Message {get; set;}
public string SessionId {get; set;}
}
public class XsiType
{
public static void Main(string[] args)
{
try
{
string filename = "XsiType.xml";
XmlSerializer s1 = new XmlSerializer(typeof(Response));
ResponseExt r = null;
using(System.IO.StreamReader reader= System.IO.File.OpenText(filename))
{
r= (ResponseExt) s1.Deserialize(reader);
}
var builder = new System.Text.StringBuilder();
var xmlws = new System.Xml.XmlWriterSettings { OmitXmlDeclaration = true, Indent= true };
using ( var writer = System.Xml.XmlWriter.Create(builder, xmlws))
{
//s1.Serialize(writer, r, ns);
s1.Serialize(writer, r);
}
string xml = builder.ToString();
System.Console.WriteLine(xml);
}
catch (System.Exception exc1)
{
Console.WriteLine("Exception: {0}", exc1.ToString());
}
}
}
related: How can I force the use of an xsi:type attribute?
(Follow-Up-Question to How to change LINQ O/R-M table name/source during runtime?)
I need to change the table source of a LINQ 2 SQL O/R-Mapper table during runtime. To achieve this, I need to create an XmlMappingSource. On command line, I could use SqlMetal to create this mapping file, but I would like to create the mapping file during runtime in memory.
The XmlMappingSource is a simple xml file, looking something like this:
<?xml version="1.0" encoding="utf-8"?>
<Database Name="MyDatabase" xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007">
<Table Name="dbo.MyFirstTable" Member="MyFirstTable">
<Type Name="MyFirstTable">
<Column Name="ID" Member="ID" Storage="_ID" DbType="UniqueIdentifier NOT NULL" IsPrimaryKey="true" IsDbGenerated="true" AutoSync="OnInsert" />
<Association Name="WaStaArtArtikel_WaVerPreisanfragen" Member="WaStaArtArtikel" Storage="_WaStaArtArtikel" ThisKey="ArtikelID" OtherKey="ID" IsForeignKey="true" />
</Type>
</Table>
<Table Name="dbo.MySecondTable" Member="MySecondTable">
<Type Name="MySecondTable">
<Column Name="ID" Member="ID" Storage="_ID" DbType="UniqueIdentifier NOT NULL" IsPrimaryKey="true" IsDbGenerated="true" AutoSync="OnInsert" />
<Column Name="FirstTableID" Member="FirstTableID" Storage="_FirstTableID" DbType="UniqueIdentifier NOT NULL" />
<Association Name="MySecondTable_MyFirstTable" Member="MyFirstTable" Storage="_MyFirstTable" ThisKey="FirstTableID" OtherKey="ID" IsForeignKey="true" />
</Type>
</Table>
</Database>
This should be possible to create using reflection, for example I can get the database name from a data context like this:
using System.Data.Linq.Mapping;
using System.Xml.Linq;
XDocument mapWriter = new XDocument();
DatabaseAttribute[] catx = (DatabaseAttribute[])typeof(WcfInterface.WaDataClassesDataContext).GetCustomAttributes(typeof(DatabaseAttribute), false);
XElement xDatabase = new XElement("Database");
xDatabase.Add(new XAttribute("Name", catx[0].Name));
mapWriter.Add(xDatabase);
My problem: I can't find good documentation of the mapping, so extracting the necessary information is quite error-prone - maybe someone can point me to good docs of the mapping, or, even better, to a code example how to create the mapping file?
Have you considered using LINQ to Entities, the mapping formats for LINQ to Entities are documented.
Use Damien Guard's Open Source T4 templates. They do everything SQLMetal can do and more, and you'll have the full T4 engine behind you.
I just had same problem, also I had no option to change project as its too late to do this.
I needed to update database name in mapping file in my solution.
This solution works.
My database mapping
<?xml version="1.0" encoding="utf-8"?>
<Database Name="DatabaseName" xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007">
<Table Name="dbo.tblDictionary" Member="TblDictionary">
<Type Name="TblDictionary">
<Column Name="lngRecordID" Member="LngRecordID" Storage="_LngRecordID" DbType="Int NOT NULL IDENTITY" IsPrimaryKey="true" IsDbGenerated="true" AutoSync="OnInsert" />
<Column Name="txtWord" Member="TxtWord" Storage="_TxtWord" DbType="VarChar(50) NOT NULL" CanBeNull="false" />
</Type>
</Table>
</Database>
and finally the code:
class Program
{
static void Main(string[] args)
{
// to get embeded file name you have to add namespace of the application
const string embeddedFilename = "ConsoleApplication3.FrostOrangeMappings.xml";
// load file into stream
var embeddedStream = GetEmbeddedFile(embeddedFilename);
// process stream
ProcessStreamToXmlMappingSource(embeddedStream);
Console.ReadKey();
}
private static void ProcessStreamToXmlMappingSource(Stream stream)
{
const string newDatabaseName = "pavsDatabaseName";
var mappingFile = new XmlDocument();
mappingFile.Load(stream);
stream.Close();
// populate collection of attribues
XmlAttributeCollection collection = mappingFile.DocumentElement.Attributes;
var attribute = collection["Name"];
if(attribute==null)
{
throw new Exception("Failed to find Name attribute in xml definition");
}
// set new database name definition
collection["Name"].Value = newDatabaseName;
//display xml to user
var stringWriter = new StringWriter();
using (var xmlTextWriter = XmlWriter.Create(stringWriter))
{
mappingFile.WriteTo(xmlTextWriter);
xmlTextWriter.Flush();
stringWriter.GetStringBuilder();
}
Console.WriteLine(stringWriter.ToString());
}
/// <summary>
/// Loads file into stream
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
private static Stream GetEmbeddedFile(string fileName)
{
var assembly = Assembly.GetExecutingAssembly();
var stream = assembly.GetManifestResourceStream(fileName);
if (stream == null)
throw new Exception("Could not locate embedded resource '" + fileName + "' in assembly");
return stream;
}`