The provided example methods all violate Single Responsibility Principle (SRP) and Separation of Concerns (SoC) so that is where I started in trying to make them more generic.
The creation of the service and service client should be abstracted out into their own concerns
For example, the web services can be created via a generic factory abstraction
public interface IWebServiceFactory {
TWebService Create<TWebService>(string uri);
}
and simple implementation which encapsulates the creation of the channel factory using the provided URL.
public class ServiceFactory : IWebServiceFactory {
public TWebService Create<TWebService>(string url) {
var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport) {
MaxReceivedMessageSize = Int32.MaxValue,
MaxBufferSize = Int32.MaxValue
};
var endpoint = new EndpointAddress(url);
var channelFactory = new ChannelFactory<TWebService>(binding, endpoint);
TWebService webService = channelFactory.CreateChannel();
return webService;
}
}
The service clients' creation can also be abstracted out into it own concern.
public interface IClientFactory {
TClient Create<TClient>() where TClient : class, new();
}
to be implemented based on the common definition of your clients.
Now for the creation of a generic client you need to take the common functionality expected from the types involved with the member to be invoked.
This can allow for a convention to be used for the expected types. Dynamic expressions were used to construct the conventions applied.
Resulting in the following helpers for the SearchClaimAsync
static class ExpressionHelpers {
public static Func<string, string, TUserResult> CreateUserDelegate<TUserResult>() {
var type = typeof(TUserResult);
var username = type.GetProperty("username", BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.Public);
var password = type.GetProperty("password", BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.Public);
//string username =>
var usernameSource = Expression.Parameter(typeof(string), "username");
//string password =>
var passwordSource = Expression.Parameter(typeof(string), "password");
// new TUser();
var user = Expression.New(type);
// new TUser() { UserName = username, Password = password }
var body = Expression.MemberInit(user, bindings: new[] {
Expression.Bind(username, usernameSource),
Expression.Bind(password, passwordSource)
});
// (string username, string password) => new TUser() { UserName = username, Password = password }
var expression = Expression.Lambda<Func<string, string, TUserResult>>(body, usernameSource, passwordSource);
return expression.Compile();
}
public static Func<TService, string, Task<string>> CreateEncryptValueDelegate<TService>() {
// (TService service, string name) => service.EncryptValueAsync(name);
var type = typeof(TService);
// TService service =>
var service = Expression.Parameter(type, "service");
// string name =>
var name = Expression.Parameter(typeof(string), "name");
// service.EncryptValueAsync(name)
var body = Expression.Call(service, type.GetMethod("EncryptValueAsync"), name);
// (TService service, string name) => service.EncryptValueAsync(name);
var expression = Expression.Lambda<Func<TService, string, Task<string>>>(body, service, name);
return expression.Compile();
}
public static Func<TClient, TUser, Task<TResponse>> CreateClaimSearchDelegate<TClient, TUser, TResponse>() {
var type = typeof(TClient);
// TClient client =>
var client = Expression.Parameter(type, "client");
// TUser user =>
var user = Expression.Parameter(typeof(TUser), "user");
var method = type.GetMethod("ClaimSearchAsync");
var enumtype = method.GetParameters()[4].ParameterType; //statuscode
var enumDefault = Activator.CreateInstance(enumtype);
var arguments = new Expression[] {
user,
Expression.Constant(string.Empty), //ssn
Expression.Constant(string.Empty), //lastname
Expression.Constant(12345), //claimnumber
Expression.Constant(enumDefault), //statuscode
Expression.Constant(string.Empty)//assignto
};
// client.ClaimSearchAsync(user, ssn: "", lastname: "", claimnumber: 12345, statuscode: default(enum), assignedto: "");
var body = Expression.Call(client, method, arguments);
// (TClient client, TUser user) => client.ClaimSearchAsync(user,....);
var expression = Expression.Lambda<Func<TClient, TUser, Task<TResponse>>>(body, client, user);
return expression.Compile();
}
}
Take some time to review the comments to get a better understanding of what is being done.
The generic web service can then be defined as follows
public class WebService<TWebServiceClient, TWebService, TUser>
where TWebService : class
where TWebServiceClient : class, new()
where TUser : class, new() {
/// <summary>
/// Create user object model
/// </summary>
private static readonly Func<string, string, TUser> createUser =
ExpressionHelpers.CreateUserDelegate<TUser>();
/// <summary>
/// Encrypt provided value using <see cref="TWebService"/>
/// </summary>
private static readonly Func<TWebService, string, Task<string>> encryptValueAsync =
ExpressionHelpers.CreateEncryptValueDelegate<TWebService>();
private readonly IWebServiceFactory serviceFactory;
private readonly IClientFactory clientFactory;
Lazy<TWebServiceClient> client;
public WebService(IWebServiceFactory serviceFactory, IClientFactory clientFactory) {
this.serviceFactory = serviceFactory ?? throw new ArgumentNullException(nameof(serviceFactory));
this.clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory));
client = new Lazy<TWebServiceClient>(() => clientFactory.Create<TWebServiceClient>());
}
public async Task<TResponse> SearchClaimAsync<TResponse>(WebServiceOptions options) {
TWebService webService = serviceFactory.Create<TWebService>(options.URL);
TUser user = createUser(
await encryptValueAsync(webService, options.UserName),
await encryptValueAsync(webService, options.Password)
);
Func<TWebServiceClient, TUser, Task<TResponse>> claimSearchAsync =
ExpressionHelpers.CreateClaimSearchDelegate<TWebServiceClient, TUser, TResponse>();
TResponse response = await claimSearchAsync.Invoke(client.Value, user);
return response;
}
//...other generic members to be done
}
public class WebServiceOptions {
public string URL { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
The code it self is decoupled enough from implementation concerns to allow for it to be tested in isolation to ensure that it behaves as expected.
As demonstrated in the following unit tested
[TestClass]
public class GenericWebServiceTests {
[TestMethod]
public void Should_Create_New_WebService() {
//Arrange
var serviceFactory = Mock.Of<IWebServiceFactory>();
var clientFactory = Mock.Of<IClientFactory>();
//Act
var actual = new WebService<WebServiceWRGClient, IWebService, User1>(serviceFactory, clientFactory);
//Assert
actual.Should().NotBeNull();
}
[TestMethod]
public async Task Should_ClaimSearchAsync() {
//Arrange
var service = Mock.Of<IWebService>();
Mock.Get(service)
.Setup(_ => _.EncryptValueAsync(It.IsAny<string>()))
.ReturnsAsync((string s) => s);
var serviceFactory = Mock.Of<IWebServiceFactory>();
Mock.Get(serviceFactory)
.Setup(_ => _.Create<IWebService>(It.IsAny<string>()))
.Returns(service);
var clientFactory = Mock.Of<IClientFactory>();
Mock.Get(clientFactory)
.Setup(_ => _.Create<WebServiceWRGClient>())
.Returns(() => new WebServiceWRGClient());
string url = "url";
string username = "username";
string password = "password";
var options = new WebServiceOptions {
URL = url,
UserName = username,
Password = password
};
var webService = new WebService<WebServiceWRGClient, IWebService, User1>(serviceFactory, clientFactory);
//Act
var actual = await webService.SearchClaimAsync<WebServiceResult>(options);
//Assert
//Mock.Get(serviceFactory).Verify(_ => _.Create<IService1>(url));
//Mock.Get(service).Verify(_ => _.EncryptValue(username));
//Mock.Get(service).Verify(_ => _.EncryptValue(password));
//Mock.Get(clientFactory).Verify(_ => _.Create<Client1>());
actual.Should().NotBeNull();
}
#region Support
public class User1 {
public string UserName { get; set; }
public string Password { get; set; }
}
public class User2 {
public string UserName { get; set; }
public string Password { get; set; }
}
public class WebServiceWRGClient {
public Task<WebServiceResult> ClaimSearchAsync(User1 user, string ssn, string lastname, int claimnumber, statuscode statuscode, string assignedto) {
return Task.FromResult(new WebServiceResult());
}
}
public enum statuscode {
NotSet = 0,
}
public class Client2 { }
public interface IWebService {
Task<string> EncryptValueAsync(string value);
}
public interface IService2 {
Task<string> EncryptValueAsync(string value);
}
public class Service1 : IWebService {
public Task<string> EncryptValueAsync(string value) {
return Task.FromResult(value);
}
}
public class WebServiceResult {
}
#endregion
}
This should be enough to get you started in reviewing the other members to be made generic. The above provided code has been tested and works as expected based on what was provided in the original question.
Do note that this does seem like a large task depending on the amount of members to be refactored. You should take some time to make sure the effort is even worth it.