Dale Preston's Web Log
  
Friday, January 27, 2006
 

Returning Classes from a Web Service in .Net 2.0


Returning custom classes from a web service, the series:

Part 1 — A public data-only class on the web service

Part 2 — A shared class library

Part 3 — Advanced methods

Part 4 — .Net 2.0 Update

Parts 1 through 3 of this series demonstrate various ways of passing custom objects from web services in the .Net framework version 1.1. The coverage may apply to framework version 1.0 as well but I haven't specifically tested them in that environment.

The articles have covered several obstacles to returning custom classes and demonstrated ways to overcome those obstacles. The .Net framework version 2.0 has built in solutions to some of those obstacles and includes some novel ways to overcome some of the others.

WSDL.exe Improvements

The version of WSDL.exe that ships with Visual Studio 2005 has eliminated the biggest problem with adding web references within Visual Studio or manually with WSDL.exe: the turning of properties into fields.

As I discussed in the previous articles, the WSDL.exe version that shipped with Visual Studio .Net 2003 converted all public properties in the classes defined in the web service into public fields in the client side proxy. The result of that conversion was that you couldn't use the proxy generated classes for databinding unless you manually edited the proxy class file, Reference.cs. Even worse, as I discovered in my research for this 4th article, Microsoft knew about the deficiency, had acknowledged that it was a deficiency, and had said it would be fixed for production, as early as Beta 1 of Visual Studio.Net 2003. Ok, I don't want to turn this article into a rant about the lack of patches or updates for known issues in Visual Studio .Net 2003 or Visual Studio 2005, so back to the topic:

Luckily, this problem has finally been solved in Visual Studio 2005. The proxies generated by the Visual Studio 2005 version of WSDL.exe include properties for all public properties and fields exposed in the web service classes.

With Visual Studio 2005 you can bind custom classes returned from a web service directly to your GridView, DropDownList, or whatever else you are binding to. For instance, you can create an ObjectDataSource and assign the return value from a WebMethod directly the ObjectDataSource in the designer:



or you can create the ObjectDataSource declaratively:

<asp:ObjectDataSource ID="NewsPeople" runat="server" SelectMethod="GetPeople"

TypeName="NewsServiceSharedClass.NewsServiceSharedClass"></asp:ObjectDataSource>


Then, assign the ObjectDataSource to the DataSourceId property of your GridView or other bindable control:

<asp:GridView ID="GridView1" runat="server" CellPadding="4" DataSourceID="NewsPeople"

ForeColor="#333333" GridLines="None" ShowFooter="True">


If all your application requires is a view of the web service output, your job is done, so far as handling custom classes from a web service goes!

The sample projects download for this article demonstrate binding GridViews to custom classes defined in the web service.

Delegation, Wrapper Classes, Inheritance, and Conversion

The advanced methods of handling custom classes discussed in the third article of this series all work the same in Visual Studio .Net 2003 and in Visual Studio 2005. For information about using those methods, see Part 3 — Advanced methods. The downloadable projects for this article demonstrates doing conversion with explicit and implicit operators as well as the enhanced proxy generation and databinding capabilities of Visual Studio 2005.

Even more advanced methods in Visual Studio 2005

Visual Studio 2005 and the .Net framework version 2.0 offers what was previously only available to the most hardcore .Net web service client coders: Custom proxy class generation, courtesy of the SchemaImporterExtension class. The SchemaImporterExtension allows us to tell WSDL.exe to substitute a local class in the proxy for a web service class advertised in the web services WSDL. The sample below demonstrates how to make WSDL.exe substitute our client's copy of NewsLibrary.Person and NewsLibrary.Reporter for the NewsService.Person and NewsService.Reporter defined in the NewsService WSDL.

I added a new class library to the NewsService2K5 solution to hold the SchemaImporterExtension class. The class is pretty simple:

using System;

using System.Collections.Generic;

using System.Text;

using System.Xml.Serialization;

using System.Xml.Serialization.Advanced;

using System.Xml.Schema;

using System.CodeDom;

using System.CodeDom.Compiler;



namespace NewsServiceSchemaImporter

{

public class NewsServiceSchemaImporter : SchemaImporterExtension

{

public override string ImportSchemaType(string name, string ns,

XmlSchemaObject context, XmlSchemas schemas, XmlSchemaImporter importer,

CodeCompileUnit compileUnit, CodeNamespace mainNamespace,

CodeGenerationOptions options, CodeDomProvider codeProvider)

{

// Check to see if we have a handler for this web service

if (string.Compare(ns, "http://dpreston.org/NewsServiceSharedClass", true) == 0)

{

// Add a reference to the NewsLibrary namespace

mainNamespace.Imports.Add(new CodeNamespaceImport("NewsLibrary"));



switch (name)

{

// Tell the extension which classes to substitute for the

// web service class.

case "Person":

return "NewsLibrary.Person";

case "Reporter":

return "NewsLibrary.Reporter";

}

}



return base.ImportSchemaType(name, ns, context, schemas, importer,

compileUnit, mainNamespace, options, codeProvider);

}

}

}

The SchemaImporterExtension.ImportSchemaType method compares the namespace being imported with the specified namespace. If they match, then the extension will generate a reference to the local NewsLibrary.Person or NewsLibrary.Reporter in place of the WebService version of those classes. If the namespace does not match, or if a class not specified in the SchemaImporterExtension is used, the extension will pass the call to the SchemaImporterExtension base and the default code generation will be used.

I found out by the accident of a typographical error, that it is not necessary to use the XmlType attribute to set the namespace of the individual classes. The namespace of the web service is what we'll match in the SchemaImporterExtension:

[WebService(Namespace = "http://dpreston.org/NewsServiceSharedClass")]

In this example of using SchemaImporterExtension, I am building on the concept introduced in the 2nd article of this series, using a common class library, NewsLibrary, for the shared classes at both the server and the client. In the Visual Studio 2005 solution, I add a reference to the class library project in both the web client and the web service applications. Both projects will maintain their own local copy of the compiled dll for NewsLibrary. Because the same class library is used the properties match and the framework is able to manage traslating the class from one namespace to another at run time with no difficulties.

You don't even have to use the same class library at the web service and the client. As long as you have a common set of properties, as Part 4 of this series discussed, you can have two different class libraries; one with the methods useful at the web service side, and another version with the methods useful at the client side.

So what the SchemaImporterExtension gives us is the same logical functionality as the conversion method described in part 3 of this series without having to write or perform casting, providing both a coding and a performance benefit.

When I searched on Google for SchemaImporterExtension while preparing for this article, I got 328 hits. There are several good articles within those 328 including a good one by Jelle Druyts at http://www.microsoft.com/belux/nl/msdn/community/columns/jdruyts/wsproxy.mspx. I could have just included a link to his article, but I wanted to complete this series by applying the SchemaImporterExtension class to the NewsService classes I have been using all along.

Using the SchemaImporterExtension

To use the SchemaImporterExtension in the NewsServiceClient, I followed Jelle's recommendation to add the extension to the GAC and added the following to the <Configuration> section of the machine.config file:

<system.xml.serialization>

<
schemaImporterExtensions>

<
add name="NewsServiceSchemaImporter"

type="NewsServiceSchemaImporter.NewsServiceSchemaImporter,

NewsServiceSchemaImporter, Version=1.0.0.0,

Culture=neutral,

PublicKeyToken=5197ce84e0543c44
" />

</
schemaImporterExtensions>

</
system.xml.serialization>

It worked exactly as advertised.

[March 20, 2007 update]
Preparing to give a presentation on the SchemaImporterExtension at work, I realized there is at least one point lacking in my explanation: to add the SchemaImporter assembly to the GAC it must have a strong name. You can create a key for generating the strong name using the strong name tool, SN.exe. Set the project properties to sign the assembly using the key generated with SN.exe. To get the PublicKeyToken value required in the configuration section above, once I signed the assembly, I dropped the assembly into the GAC on my pc. In the GAC, I was able to see the PublicKeyToken listed. I noted the PublicKeyToken, and then removed the assembly from the GAC and assigned the noted value in the configuration section. There may be other, easier, better ways to get the PublicKeyToken but I was not aware of them except for having found that the token is the last 8 bytes (16 hex numbers) of the SHA-1 hash of the public key generated with SN.exe.
[End March 20, 2007 update]

Jelle's article talks about some of the problems with using SchemaImporterExtensions on a web site application because the WSDL proxy classes are not generated until compile time. For this reason and because I am in one of those environments he talks about in which it is very difficult to add assemblies to the GAC and to modify machine.config, I tried a few experiments.

I removed the assembly from the GAC and set a reference to the project in my solution which worked great on my local machine indicating that it should work well if the extension is pre-compiled into your web site appliction before deployment. This would, at least in my environment at work, require a separate development/build machine configured just like the test/production machines we use now except that, on the build machine, you would add the configuration section to machine.config, compile your application, and deploy the compiled application to the test server.

Next, I removed the XmlSerialization section from machine.config and added it to the web.config of the NewsServiceClient web application. The application compiled and ran without error. Since the SchemaImporterExtension is only used at compile time to create the proxy class for the WSDL, then we know already that this test was successful as well.

Technically, it is still impossible to move an instance of a custom class from the web service to the client. In this series of articles I have shown several ways in .Net 1.1 and 2.0 to simulate passing a custom class so seamlessly that you'd never know the difference.

Comments:
Great articles! I'm curious how you got the project reference to work since it doesn't seem to build the proxy until you run the application.
 
I'm not quite sure what you're asking. If you download and build the samples, you can see that the references do work. While the proxy class is not created until later, the WSDL can still be used to validate the code for the compiler.
 
Nice articles.
It almost completly answer my questions...
Almost because i have a problem.
I'm webreferencing my WebServices not in a WebApplication but in a class library... When doing so, an app.config is created with the webservice url setting.
I tried to add the system.xml.serialization section in it but my SchemaImporterExtension is ignored... any ideas?
My Extension work well when calling wsdl.exe manually...
 
Okay i found my answer, for a class library project the Extension must be in GAC and declared in machine.config. Any declaration in app.config is ignored.
I guess web.config is handled differently.
 
Hi,

is it even possible to use the SchemaImporterExtension in a normal (not a web) project? I would like to use a web service in a normal project and just do "Add web reference" in VS. So in which config file do I have to place the "system.xml.serialization"-section to tell VS that it has to use my SchemaImporterExtension?

Thanks in advance
 
Just as an update, the SchemaImporterExtension class does not work when using the Project add-in for web projects.

The SchemaImporterExtension works by controlling the proxy generation process at compile time. The project add-in causes the web service proxy class to be generated when the web reference is added to the project instead of at compile time - therefore no SchemaImporterExtension controlled proxy.
 
Anonymous - I assume you have probably gotten your answer by now, but for future viewers, I'll respond even though late.

By "normal project" I assume you mean Windows Forms - though, for me, a web app is my "normal project".

I haven't used the SchemaImporterExtension in a Windows Forms app but, as Vincent said above, he was able to use the SchemaImporterExtension in a class library by adding the extension dll to the GAC and adding the configuration code to machine.config. The same thing should work in a Windows Forms project.
 
Just a quick comment for those wondering about using the SchemaImporterExtension in non-web applications. While I added my configuration section to the web.config file so that it would work for any developer who opens the project, there really is no need to, per se. The SchemaImporterExtension only comes into play at compile time. You could add the extension to your own GAC without affecting any deployed web or application servers. If you use a build server, you might want to add the extension to the GAC of the build server but if that is not possible, add it to the web.config for a web app like I did.
 
I have just come across your page as this is exactly what I am trying to do. I downloaded your sample app but couldn't get this to compile. The key.snk file was missing from the solution and when I try to compile I get the following error:
Error 1 Initialization failure. Please review input options and documents for validity. App_WebReferences/NewsServiceSharedClass/

Is there something else that I should be doing to get this working?
 
There is an update I posted a couple days ago that talks about using SN.exe to give the app a strong name. Search for [March 20, 2007 update].

Drop the reference in the downloaded project to key.snk and use SN.exe to create key.snk. Add a reference to your own version. Right-click on the project file in solution explorer and choose Properties. In the signing tab, choose your key.snk file (or whatever you name your key file) to get the strong name for your file. Then you need to get the PublicKeyToken as described above.

Coincidentally, I did a presentation today on that series of articles and have created a new set of demo applications in .Net 2.0 (they require VS2005 SP1 or the Web Application Project update to VS2005) except for NewsServiceDemo4 which will not work as a web application project. It must be a website project.

NewsServiceDemo4 does include the key.snk file but read the project readme file to see how to deal with some issues - about 2 minutes work to resolve any issues in making the project build and run.

You can download the entire set at http://www.dalepreston.com/downloads/objectsfromwsdemos.zip
 
Great info, Dale! Thanks for getting me off in the right direction. I'm having trouble though...

I have downloaded your sample; it compiles and runs fine.

I have created a new Solution, complete with my own SchemaImporter override class, WebService, Custom Class, and Consumer website - it all compiles and runs fine.... on the first try.

When I try to add an additional custom class, or move the SchemaImporter over to an existing solution (changing Namespaces and updating Web References ad nauseum) I can't get it to compile.

Even changing everything back to the original values does not correct the problem - I'm still plagued by the "Cannot implicitly convert..." error.

Are there temp files representing the proxy classes left over someplace, or something else I'm missing?

I've been working on this for 4 days now and the only solution I have is starting from scratch each time... ridiculous.

Any thoughts are appreciated.
 
Without code, it would be hard to say what is wrong. What comes to mind is that you have to make sure that you add each class to the switch statement in the SchemaImporterExtension. Next you may have to update your web reference. Even in-solution web references don't always keep up, though they should.

Dale
 
Hi Dale,
Good stuff!
But what happens if I don't use proxy generated by WSDL? Simply I just add web reference from VS2005, it doesn't create any proxy for you.

Thanks.
William
 
I was wrong, it does create .cs file, just not shown.

Thanks.
william
 
Hi Dale,

I've followed steps you mentioned, but I got error about machine.config file. Here is my case:
1. I put WSSchemaImporter class in assembly SSS.Atrieve.FFF.Common.dll, the class looks like this:
namespace SSS.Atrieve.FFF.Common{
public class WSSchemaImporter:SchemaImporterExtension{
public override string ImportSchemaType(...){...}
}
}
2. I strong named it, and put it into GAC.
3. Added schemaimportersection right after "Configuration" section in machine.config,like this:
"configuration"
"system.xml.serialization"
"schemaImporterExtensions"
"add name="SSS.Atrieve.FFF.Common.WSSchemaImporter" type="SSS.Atrieve.FFF.Common.WSSchemaImporter, SRB.AtrieveERP.FIS.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=abb3100936f96b37" /"
"/schemaImporterExtensions"
"/system.xml.serialization"
...
When I tried to add web reference of my web service from windows application, I got error "Unrecognized configuration section system.xml.serialization.".
Please note that I have to use double quote for tag since Tag is not allowed to upload in your blog.

Can you help me out?

Thanks.
William
 
William, The first section in your machine.config must be the configSections section - otherwise, the framework does not know how to process the system.xml.serialization section. Move the system.xml.serialization below the configSections section.
 
Thanks Dale,

Great! It works after moving it to after configSections section.
I wonder why it doesn't work if I added the serialization config in windows applicaiton config file instead of machine.config.(I tried once). In production environment, it's better that having this kind of config information with applicaiton's own config file, instead of machine.config. Any idea about this?

Thanks.

William.
 
Hi Dale,
I have another question regarding adding web reference of web service in web application. basically I want to try moving that config section into web.config file. So:
1. my web service was developed in VS2005 in C# as web site,
[WebService(Namespace = "http://www.CompanyName.com/")]
public class Login : System.Web.Services.WebService {
public Login(){}
[Webmethod]
Public string GetUserInfo(...){...}
...}
2. I deployed it on my local XP machine as virtual directory "TestWebService".
3. I created a test web application by File>New>Web Site>ASP.NET Web Site,
name it TestWebSite.
4. I added web reference from my web test application which is in VS2005 C#,
renamed the reference name to "TestLoginServ".
5. Added a button on the Default.aspx, and in button1_click event, tried to
use TestLoginServ.Login class by
TestLoginServ.Login login=new TestLoginServ.Login();
but TestLoginServ.Login is not recognized.

Any idea?
I just don't want to create proxy manually.

Thanks.

William
 
William,

In answer to your first question, you should be able to add the xmlSerialization section to your app.config or web.config. If you download the demo projects from this series, you can see that I do that in the demo for the SchemaImporterExtension. Adding the section to the machine config is not an option where I work - the infrastructure guys guard the GAC and machine.config very tightly.

In your second question, the problem with your Login class is most likely that you do not have a WebMethod that returns a Login or uses a Login type as a parameter. Create a dummy void method that takes a Login parameter and update your web references. The WSDL code Microsoft uses for web references will not generate proxies for types that are not used in a WebMethod.
 
Hi Dale,
Thanks for your response. I appreciate.
I've tried all out.
1. On test/production environment, you don't need to deal with adding config anywhere, neither adding the extension assembly to GAC as long as it's available.
2. On development environment, change machine.config and add assembly to GAC as you said in your article(all developers should be able to control their own machine), it's durable.
3. Since opinion 1, this approach works for all types of clients(dll,win form,web page).
Hope this make it clear for other visitors.
By the way, please ignore my last question since I made mistake.

Have a good day.

William
 
Hi Dale,

Nice articles! I ran into this because I got a problem while I was migrating my web service client from .NET 1.1 to .NET 2.0 [Well I know I am little late in this but as said don't change if its working]. Migrating .sln and .vcproj file was piece of cake and everything compiled properly as well.

But it started giving problem during runtime and the reason was I have created my own types in 1.1 and these are now conflicting with types in 2.0.

For example:

ArrayOfString
ArrayOfInt and so on.

.NET 2.0 provides the same name for these types and it is now conflicting and throwing reflection error and eventually boiling down to this error message

One solution is to change my type names i.e. from ArrayOfString to XXXArrayOfString but this will require to change all the users of my web services which is not a good solution.

I have looked at this article but of no use

http://social.msdn.microsoft.com/forums/en-US/asmxandxml/thread/abba7508-b126-4c6e-9735-c3ae9e57b64a/

I guess, there should be some simple solution to deal with this. I would appreciate if you please inform me how to go about it.

Dipesh
 
Hi Dale.

I see you are the master of web services, so PLEASE help me with this one (I've spent already 2 days):

I have a simple web svc... 1 method that returns my object:

[WebMethod]
[XmlInclude(typeof(Person))]
public PersonAction GetAction()
{
PersonAction action = new PersonAction();
return action;
}

then the classes:

[Serializable]
public class Person
{
public string name;
public Person() { }

public string Name
{
get
{
return this.name;
}
set
{
this.name = value;
}
}
}

[Serializable]
public class PersonAction
{
public string actionName = "XAction";
public Person person1;
public Person person2;

public PersonAction()
{
this.person1 = new Person();
this.person1.Name = "P1";
this.person2 = new Person();
this.person2.Name = "P2";
}

public string Name
{
get
{
return this.actionName;
}
}
public Person Person1
{
get
{
return this.person1;
}
}
public Person Person2
{
get
{
return this.person2;
}
}

In the wsdl there is only this empty "PersonAction" tag.

Do you have an solution/explanation? I cannot figure it out.

Thank you in advance!
 
Post a Comment

<< Home

Powered by Blogger