tags:

views:

466

answers:

4

I have 3 classes that are essentially the same but don't implement an interface because they all come from different web services.

e.g.

  • Service1.Object1
  • Service2.Object1
  • Service3.Object1

They all have the same properties and I am writing some code to map them to each other using an intermediary object which implements my own interface IObject1

I've done this using generics

public static T[] CreateObject1<T>(IObject1[] properties)
  where T : class, new()
{
   //Check the type is allowed
   CheckObject1Types("CreateObject1<T>(IObject1[])", typeof(T));
   return CreateObjectArray<T>(properties);
}

private static void CheckObject1Types(string method, Type type)
{
  if (type == typeof(Service1.Object1)
  || type == typeof(Service2.Object1)
  || type == typeof(Service3.Object1)
  || type == typeof(Service1.Object1[])
  || type == typeof(Service2.Object1[])
  || type == typeof(Service3.Object1[]))
  {
     return;
  }

  throw new ArgumentException("Incorrect type passed to ServiceObjectFactory::" + method + ". Type:" + type.ToString());
}

My client code looks like:

//properties is an array of my intermediary objects
Object1[] props = ServiceObjectFactory.CreateObject1<Object1>(properties);

What I want to do is get rid of the CheckObject1Types method and use constraints instead so that I get a build error if the types aren't valid, because at the moment I can call this method with any type and the ArgumentException is thrown by the CheckObject1Types method.

So I'd like to do something like:

public static T[] CreateObject1<T>(IObject1[] properties)
  where T : class, new(), Service1.Object1|Service2.Object1|Service3.Object1
{
   return CreateObjectArray<T>(properties);
}

Any ideas?

Edit: I don't want to change the Reference.cs files for each webservice because all it takes is a team mate to update the web reference and BAM! broken code.

+1  A: 

Constraining to a list of classes in an "OR" fashion like you want to do isn't possible in C#. (In fact, I'm not even sure it's legal directly in IL either.)

Your only option is to keep using the checktypes style functions. If you own the code for the different webservices, you can also implement a "sentinel" interface and use that as your constraint. I know sentinel interfaces are not recommended practice per the Framework Design Guidelines, but they occasionaly have their uses (this being one of them).

As Jon points out, you may be able to make use of prtial classes in order to implement a common interface. If your References.cs implements a class:

namespace TestServices
{
   internal partial class Service1SoapClient : System.ServiceModel.ClientBase<T>, K
   {
   }
}

You would then create another partial class in the same namespace (call it References.CommonInterface.cs), which can live anywhere in your project, with the following:

namespace TestServices
{
   internal interface ICommon
   {
   }

   internal partial class Service1SoapClient : ICommonInterface
   {
   }
}
Scott Dorman
Fair enough, I hope you're proved wrong though!Any expansion as to why this is? I don't own the code - yes I could go through the Reference.cs and manually assign each class to a common interface but if a team mate updates the web references - BAM! No more interface.
Rob Stevenson-Leggett
See Jon Skeet's answer regarding the possibility of using partial classes to derive everything from a common interface.
Scott Dorman
See this article on why a "switch on type" capability wasn't implemented, which probably has many of the same reasons. http://blogs.msdn.com/peterhal/archive/2005/07/05/435760.aspx
Scott Dorman
A: 

If you're pulling these objects from a web service you ulitmately do have control over the class definitions used. They don't just spring up out of thin air (even if you do have code generator or visual studio creating them initially). There's still a class file for each of them somewhere that must be compiled with the app, and you should be able to add your common interface to those class definitions.

Joel Coehoorn
See my comment on Scott Dorman's answer.
Rob Stevenson-Leggett
Isn't that what source control is for?
Joel Coehoorn
True, I just don't like to do that I guess. Code smell?
Rob Stevenson-Leggett
Even with source control, everytime you make a change to reference.cs it would get recommitted. Someone would still have to go and manually revert to the "Interfaced" copy...
Ricardo Villamil
+13  A: 

Assuming the generated classes are partial, you can create an interface and then add another partial source file to make your generated classes implement the interface. Then you can constrain by interface as normal. No changes to the actual generated code required :)

Jon Skeet
That's kinda what I was trying to get at.
Joel Coehoorn
Good point about the partial class trick. I just looked at a new project I created in VS2008 (.NET 3.5) using a service reference and it did create the client as a partial class.
Scott Dorman
Interesting... I'll give this a try.
Rob Stevenson-Leggett
Good idea. I didnt realize you actually had access to the source code of the service agent. This would be an ideal solution to your problem. Upvote from me
Kilhoffer
This is a neat trick!
Vivek
Jon, have you posted an example of this on your (fantastic) website? I'd be very interested in seeing it.
endian
@endian: I don't have an example on the website, but I've done a similar thing in Protocol Buffers recently. So see http://github.com/jskeet/dotnet-protobufs/tree/master/csharp/ProtocolBuffers/DescriptorProtos/PartialClasses.cs for an example :)
Jon Skeet
A: 

I would write a converter class that took either of your three objects into a new object that supports the interface you want. Furthermore, I would use reflection so you don't have to type all the assignments manually (unless it's a small object and is not expected to change too much).

Using Reflection would also give you the guarantee that you want to make sure the objects implement whatever properties your new Interfaced object implements, otherwise when a property you expect is not implemented you could have it throw an error.

Ricardo Villamil