Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In C#, a class could return two different types of objects, need to loop through them

Tags:

c#

I'm not new to C# but I haven't really used generics and DI much but I've come across a project to teach myself so I'm taking this opportunity. Here's the scenario (will be long and I thank you for your patience)...

We have an on-prems solution which hosts webservices. I have an app that will hit these webservices to pull information and generate data files for one of our partners. We're moving from the on-prems solution to a cloud-based solution (same vendor, same webservices, slightly different implementation in the cloud). I decided it would be best to learn interfaces (more than I know now), generics (which I'm learning as I type this), and dependency injection (also a beginner in this specific area) by doing the following

Create an interface for each webservice I'm calling i.e. IGetUsers with a function CallUserService returning List Implement the interface once each for the cloud solution and on-prems solution

public class GetUsersCloud : IGetUsers { public List<string> CallUserService(int ID)... }

public class GetUsersPrems : IGetUsers { public List<string> CallUserService(int ID)... }

Now I have a class with a constructor that takes an instance of IGetUsers

public class GetUsers {        
    private IGetUsers _getUsers;
    public GetUsers(IGetUsers getUsers) {
        _getUsers = getUsers;
}

From here I can either instantiate this class with an instance of the class that handles the on-prems solution or the cloud solution and call a

GetUsers getUsers;
if (!cloud) {
     getUsers = new GetUsers(new GetUsersPrems());
} else {
     getUsers = new GetUsers(new GetUsersCloud());
}
List<string> userList = getUsers.CallUserService(5);

This works great because my return type is a List so I can handle this very easily. Here's where I'm getting slightly stuck.

I have another interface to implement but the webservice that the classes that will implement it will return different types. What I mean is that while the two webservices will return a type called UserDetailList, they're coming from two different strongly typed classes created by importing the service references from the on-prems and the cloud solution. To handle this, I created an interface with a function that returns a generic

public interface IUserDetails {
    T GetUserDetails<T>(int ID);
}

My class implementations look like this (clearly missing the meat and potatoes but you get the idea)

public class GetUserDetailsCloud : IUserDetails {
    public T FetchUserDetails<T>(int ID) { 
          CloudWS.UserDetailList userDetailList = cloudWebservice.GetUserDetailList(ID);
          return (T)(object)userDetailList;
    }
}

public class GetUserDetailsPrems : IUserDetails {
    public T FetchUserDetails<T>(int ID) { 
          PremsWS.UserDetailList userDetailList = premsWebservice.GetUserDetailList(ID);
          return (T)(object)userDetailList;
    }
}

I create a class with a constructor that takes an instance of IUserDetails

public class GetUserDetails{        
    private IUserDetails _getUserDetails;
    public GetUserDetails(IUserDetails getUserDetails) {
        _getUserDetails= getUserDetails;            
    }
}

Now it's easy, I can instantiate based on whether I'm on cloud or not:

GetUserDetails getUserDetails;     
object userDetailsResults;
if (!cloud) {
     getUserDetails = new GetUserDetails (new GetUserDetailsPrems());
     userDetailsResults = getUserDetails.FetchUserDetails<PremsWS.UserDetailList>(ID);  
} else {
     getUserDetails = new GetUserDetails (new GetUserDetailsCloud());
     userDetailsResults = getUserDetails.FetchUserDetails<CloudWS.UserDetailList>(ID);
}

This is where I get stuck. Since the two webservices have different types, I call it and pass the type based on whether I'm cloud or not and then my results are stored in an object instead of being a instance of the strongly typed class. My next steps involve looping through the results and performing operations/populating an output file. I used to be able to say, with the one type...

foreach (PremsWS.UserDetail in userDetailsResults) {
    <do some trivial stuff here>
}

Now It seems to me that I'd have to write two separate functions to loop through the data and process the results. I feel like there should be a way for me to write a function that takes in a generic type and then depending on what it is (whether an instance of the cloud or on-prems), process the results.

I've been sitting here trying to figure it out and my google has failed me on finding a tutorial or anything that could point me in the right direction I'd appreciate it.

like image 689
mitzman Avatar asked Nov 06 '22 13:11

mitzman


1 Answers

The ideal path here would be to just return the same type from GetUserDetailList. Since that seems like it isn't an option, considering both return types to be the same name has done a little bit of harm in realizing their separation.

They may as well be returning type Web and Local. In addition, it would seem you do not have the ability to alter the UserDetailList type.

As a result, the only place you can introduce a fix is at the point of consumption. Once you have the strongly typed instance, you will need to abstract over it. The best way here, since you know the properties at least, is to use a copy constructor to facilitate the abstraction.

public class LocalUserDetailList
{
    public [[ underlying name and type for the UserList data ]] { get; set; }

    public LocalUserDetailList(PremsWS.UserDetailList UserDetailList)
    {
        // populate the publicly available list above^
    }

    public LocalUserDetailList(CloudWS.UserDetailList UserDetailList)
    {
         // populate the publicly available list above^
    }
}

Now you can use this copy constructor in order to return a unified type to consume.

public interface IUserDetails 
{
    LocalUserDetailList FetchUserDetails(int ID);
}

public class GetUserDetailsCloud : IUserDetails {
    public LocalUserDetailList FetchUserDetails(int ID) { 
          CloudWS.UserDetailList userDetailList = cloudWebservice.GetUserDetailList(ID);
          return new LocalUserDetailList(userDetailList);
    }
}

public class GetUserDetailsPrems : IUserDetails {
    public LocalUserDetailList FetchUserDetails(int ID) { 
          PremsWS.UserDetailList userDetailList = premsWebservice.GetUserDetailList(ID);
          return new LocalUserDetailList(userDetailList);
    }
}

And finally access the unified instance without object from your call.

GetUserDetails getUserDetails;     
LocalUserDetailList userDetailsResults;
if (!cloud) {
    getUserDetails = new GetUserDetails (new GetUserDetailsPrems());
    userDetailsResults = getUserDetails.FetchUserDetails(ID);  
} else {
    getUserDetails = new GetUserDetails (new GetUserDetailsCloud());
    userDetailsResults = getUserDetails.FetchUserDetails(ID);
}

While this does not use generics, it does work, and is easy to read and debug. Sometimes, generics can become sloppy, and in this instance it would be. It would require that you use a properties iteration in order to get the values during iteration had you kept using object as the type. That is not ideal, is slow, and can cause the code to be very hard to read.

In addition, using an abstraction such as this should also allow you to realize places in your code where you can remove the duplication of calls and just return the one copy constructor instance. If you were to then leverage your abstracted version with generics, that would be a much more powerful and integrated way of using that tool.

like image 133
Travis J Avatar answered Nov 15 '22 00:11

Travis J