Как я могу сделать несколько вызовов веб-службы SOAP универсальными, чтобы уменьшить избыточность?

У меня есть эти методы ниже, которые вызывают некоторые веб-службы SOAP, все от одного и того же провайдера, поэтому все они имеют одни и те же методы/вызовы и т. д. Я ищу более ООП/абстрактный способ вызвать их, не написав так много методов? В идеале мне нужен один метод для каждого -> GetClaim(), AddClaim(), SearchClaim(), RemoveClaim() и т. д.

Вопрос. Должен ли я передавать параметры, характерные для службы, чтобы сделать метод более общим, исключая 15 других методов, подобных этому, или есть лучший более абстрактный подход? Может ли кто-нибудь привести мне пример?

    // ex. how can I make these two methods 1?
    public async void ClaimSearchForWRG(string url, string userName, string password) {
        var client = new WebServiceWRGClient(); 
        var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
        var endpoint = new EndpointAddress(url); 
        var channelFactory = new ChannelFactory<WebServiceWRG>(binding, endpoint); 
        var webService = channelFactory.CreateChannel();
        var user = new User(); 
        user.UserName = await webService.EncryptValueAsync(userName);
        user.Password = await webService.EncryptValueAsync(password);
        var response = await client.ClaimSearchAsync(user, "", "", 12345, statuscode.NotSet, "");
    }

    // another call (same provider) with the same call ->  ClaimSearchAsync()
    public async void ClaimSearchForAWI(string url, string userName, string password) {
        var client = new WebServiceAWIClient(); 
        var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
        var endpoint = new EndpointAddress(url); 
        var channelFactory = new ChannelFactory<WebServiceAWI>(binding, endpoint); 
        var webService = channelFactory.CreateChannel();
        var user = new ArmUser(); 
        user.UserName = await webService.EncryptValueAsync(userName);
        user.Password = await webService.EncryptValueAsync(password);
        var response = await client.ClaimSearchAsync(user, "", "", 12345, ArmStatuscode.NotSet, "");
    }
    // then we have 15 other web service calls from the same provider for ClaimSearchAsync()
    // then we have 15 more calls for ClaimGetAsync()
    // then we have 15 more calls for AddClaimAsync()
    // then we have 15 more calls for RemoveClaimAsync()
    // etc, etc, etc

ОБНОВЛЕНО После того, как я попробовал этот код ниже, чтобы сделать вещи немного более общими (чтобы устранить избыточность), я получаю некоторые ошибки в коде. В частности, компилятор не находит свойства, связанные с универсальными сущностями, которые я передаю в метод. бывший. user.Username не найден -> в сообщении об ошибке говорится, что «TTwo» не содержит определения для «UserName»

    public class Test {
        public void TestWebService() {
            var ws = new WebService<WebServiceWRG>();
            ws.SearchClaim(new WebServiceWRGClient(), new GraceUser(), 
                "https://trustonline.delawarecpf.com/tows/webservicewrg.svc", "userName", "password");  
        }
    }

    public class WebService<T> {             
        public void SearchClaim<TOne, TTwo>(TOne entity1, TTwo entity2, string url, string userName, string password) 
            where TOne : class
            where TTwo : class
        {
            var client = entity1;
            var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
            var endpoint = new EndpointAddress(url);
            var channelFactory = new ChannelFactory<T>(binding, endpoint);
            var webService = channelFactory.CreateChannel();
            var user = entity2;
            user.UserName = webService.EncryptValue(userName);
            user.Password = webService.EncryptValue(password);
            var response = client.ClaimSearch(user, "", "", 12345, GraceStatuscode.NotSet, "");
        }
    }

ОБНОВЛЕНО Меня попросили показать, что делает ClaimSearchAsync или что это такое. Я скопировал это из справочного файла веб-службы, созданного из dotnet.

System.Threading.Tasks.Task<GRACE_GRACES.WebServiceResult> ClaimSearchAsync(GRACE_GRACES.User user, string ssn, string lastname, int claimnumber, GRACE_GRACES.statuscode statuscode, string assignedto);

поскольку это веб-служба, за ней нет метода или кода, показывающего, что она делает.


person user1186050    schedule 24.09.2019    source источник
comment
Чтобы узнать, что делает ClaimSearch() или что представляет собой клиентский объект, вам нужно обратиться к веб-службе trustonline.delawarecpf.com/tows/webservicewrg.svc и что он генерирует, когда вы добавляете его в качестве веб-службы в свой проект. Если нет другого способа показать вам, это единственный способ, который я знаю. Но я опубликую то, что вижу в сгенерированном файле веб-сервиса.   -  person user1186050    schedule 30.09.2019
comment
вы должны сказать, что второй объект имеет свойства UserName и Password ->, либо определив базовый класс, либо назначив интерфейс, который имеет два свойства. Если ваш TTwo сгенерирован интерфейсом soap, он имеет модификатор partial -›, вы можете просто добавить еще один файл с тем же разделяемым классом (= то же пространство имен и имя, другое имя файла) и добавить интерфейс к вашему частичному классу.   -  person CitrusO2    schedule 01.10.2019
comment
Это может быть полезно Источник   -  person champion-runner    schedule 03.10.2019


Ответы (4)


Все представленные примеры методов нарушают Принцип единой ответственности (SRP) и Разделение ответственности (SoC), поэтому я начал с того, что пытался сделать их более общими.

Создание сервиса и клиента сервиса должно быть абстрагировано от их собственных интересов.

Например, веб-сервисы могут быть созданы с помощью общей фабричной абстракции.

public interface IWebServiceFactory {
    TWebService Create<TWebService>(string uri);
}

и простая реализация, которая инкапсулирует создание фабрики каналов с использованием предоставленного 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;
    }
}

Создание клиентов службы также может быть абстрагировано в свою собственную заботу.

public interface IClientFactory {
    TClient Create<TClient>() where TClient : class, new();
}

для реализации на основе общего определения ваших клиентов.

Теперь для создания универсального клиента вам необходимо использовать общую функциональность, ожидаемую от типов, связанных с вызываемым членом.

Это может позволить использовать соглашение для ожидаемых типов. Для построения применяемых соглашений использовались динамические выражения.

В результате появились следующие помощники для 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();
    }
}

Потратьте некоторое время на просмотр комментариев, чтобы лучше понять, что делается.

Затем общий веб-сервис можно определить следующим образом.

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; }
}

Сам код достаточно отделен от проблем реализации, чтобы его можно было протестировать изолированно, чтобы убедиться, что он ведет себя так, как ожидалось.

Как показано в следующем блоке, протестированном

[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
}

Этого должно быть достаточно, чтобы вы начали просматривать другие элементы, чтобы сделать их общими. Приведенный выше код был протестирован и работает, как и ожидалось, на основе того, что было предоставлено в исходном вопросе.

Обратите внимание, что это кажется большой задачей в зависимости от количества элементов, подлежащих рефакторингу. Вы должны потратить некоторое время, чтобы убедиться, что усилия того стоят.

person Nkosi    schedule 30.09.2019
comment
привет Нкоси. Спасибо за помощь. Мне понадобится некоторое время, чтобы просмотреть это и понять это. Я буду работать над этим завтра, так как сегодня вечер воскресенья, и я очень устал. Я дам вам знать, если возникнут какие-либо проблемы, и если это то, что мне нужно и помогло мне. В любом случае я, вероятно, награжу вас баллами за подробный ответ. - person user1186050; 30.09.2019

У вас здесь классический запах Divergent Change.

Признаки и симптомы. При внесении изменений в класс вам приходится менять множество несвязанных методов. Например, при добавлении нового типа товара необходимо изменить методы поиска, отображения и заказа товаров.

Предлагаю сделать рефакторинг по шаблону Abstract Factory. Вы разделите логику создания веб-сервиса и объекта.

Абстрактная фабрика — это творческий шаблон проектирования, который позволяет создавать семейства связанных объектов без указания их конкретных классов.

Таким образом, у вас будет что-то вроде: UML-диаграмма

И немного кода:

    public interface IFactory
    {
        Client CreateClient();
        User CreateUser();
        Channel CreateChannel(BasicHttpBinding binding, EndpointAddress endpoint);
    }

    abstract public class AbstractFactory<T> : IFactory
    {
        public abstract Client CreateClient()
        public abstract User CreateUser();
        public Channel CreateChannel(BasicHttpBinding binding, EndpointAddress endpoint)
        {
            var channelFactory = new ChannelFactory<T>(binding, endpoint);
            return channelFactory.CreateChannel();
        }
    }

    public class AWIFactory : AbstractFactory<WebServiceAWI>
    {
        public override Client CreateClient()
        {
            return new WebServiceAWIClient();
        }

        public override User CreateUser()
        {
            return new ArmUser();
        }
    }

    public class WRGFactory : AbstractFactory<WebServiceWRG>
    {
        public override Client CreateClient()
        {
            return new WebServiceWRGClient();
        }

        public override User CreateUser()
        {
            return new User();
        }
    }

    public class WebService
    {
        private readonly IFactory _factory;

        public WebService(IFactory factory)
        {
            _factory = factory;
        }

        public async void ClaimSearchAsync(string url, string userName, string password)
        {
            var client = _factory.CreateClient();
            var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
            var endpoint = new EndpointAddress(url);
            var channel = _factory.CreateChannel(binding, endpoint);
            var user = _factory.CreateUser();
            user.UserName = await channel.EncryptValueAsync(userName);
            user.Password = await channel.EncryptValueAsync(password);
            var response = await client.ClaimSearchAsync(user, "", "", 12345, statusCode, "");
        }

        ...
    }

А вот как вы создаете WebService:

var wrgWebService = new WebService(new WRGFactory());
person exxbrain    schedule 03.10.2019

Я сделал что-то подобное, когда у меня было несколько разных конечных точек мыла, где у каждой конечной точки были некоторые типы, которые были совершенно одинаковыми, только с другим именем класса. Автоматически сгенерированные классы содержат модификатор partial, который позволяет добавлять в сгенерированный класс дополнительную логику.

В твоем случае:

"'TTwo' не содержит определения для 'UserName'"

Вы должны создать интерфейс, который содержит свойство Username и свойство Password:

public interface IUser {
    string UserName { get; }
    string Password { get; }
}

public partial User : IUser { }  //must be in the correct namespace for partial to work
public partial ArmUser : IUser { } //must be in the correct namespace for partial to work

public class Test {
    public void TestWebService() {
        var ws = new WebService<WebServiceWRG>();
        ws.SearchClaim(new WebServiceWRGClient(), new GraceUser(), 
            "https://trustonline.delawarecpf.com/tows/webservicewrg.svc", "userName", "password");  
    }
}

public class WebService<T> {             
    public void SearchClaim<TOne, TTwo>(TOne entity1, TTwo entity2, string url, string userName, string password) 
        where TOne : class
        where TTwo : IUser  // limits the TTwo class to implement IUser
    {
        var client = entity1;
        var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
        var endpoint = new EndpointAddress(url);
        var channelFactory = new ChannelFactory<T>(binding, endpoint);
        var webService = channelFactory.CreateChannel();
        var user = entity2;
        user.UserName = webService.EncryptValue(userName);
        user.Password = webService.EncryptValue(password);
        var response = client.ClaimSearch(user, "", "", 12345, GraceStatuscode.NotSet, "");
    }
}

Вместо передачи TTwo вы также можете добавить модификатор new к условию TTwo (where T : TTwo, new()), после чего вы можете сгенерировать экземпляр TTwo внутри функции SearchClaim, что сделает его следующим:

public interface IUser {
    string UserName { get; set; }
    string Password { get; set; }
}

public partial User : IUser { }  //must be in the correct namespace for partial to work
public partial ArmUser : IUser { } //must be in the correct namespace for partial to work

public class Test {
    public void TestWebService() {
        var ws = new WebService<WebServiceWRG>();
        ws.SearchClaim(new WebServiceWRGClient(), new GraceUser(), 
            "https://trustonline.delawarecpf.com/tows/webservicewrg.svc", "userName", "password");  
    }
}

public class WebService<T> {             
    public void SearchClaim<TOne, TTwo>(TOne entity1, string url, string userName, string password) 
        where TOne : class
        where TTwo : IUser, new()  // limits the TTwo class to implement IUser
    {
        var client = entity1;
        var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
        var endpoint = new EndpointAddress(url);
        var channelFactory = new ChannelFactory<T>(binding, endpoint);
        var webService = channelFactory.CreateChannel();
        var user = new TTwo();
        user.UserName = webService.EncryptValue(userName);
        user.Password = webService.EncryptValue(password);
        var response = client.ClaimSearch(user, "", "", 12345, GraceStatuscode.NotSet, "");
    }
}

Возможно, вам также придется сделать какой-нибудь интерфейс для вашего TOne, но вы сможете разобраться с этим самостоятельно.

person CitrusO2    schedule 04.10.2019

Не уверен, какова ваша структура кода, но я просто сосредоточусь на предоставленном образце.

Из того, что я видел, если этот метод и другие связанные методы используются в разных классах, я бы предложил создать класс, который будет его обрабатывать, а затем использовать этот класс вместо методов. Но если этот метод и другие связанные методы используются в конкретном классе, я бы рекомендовал создать общие методы, которые заменят избыточные методы. Сначала вам нужно будет сравнить все связанные методы и получить общий числитель между ними, сделать это отправной точкой для вашего общего подхода.

Вот непроверенный пример (на основе того, что я понял из вашего образца):

public class CallWebService<T> // don't forget to inherit IDisposal.
{

    private WebServiceWRGClient Client {get; set;}

    private BasicHttpBinding HttpBinding {get; set;}

    private EndpointAddress  Endpoint {get; set;}

    private ChannelFactory Channel {get; set;}

    // if needed outside this class, make it public to be accessed globally. 
    private User UserAccount {get; set;}

    public CallWebService<T>(string url)
    {
        Client      = new WebServiceWRGClient(); 

        //See which Binding is the default and use it in this constructor. 
        HttpBinding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);

        Endpoint    = new EndpointAddress(url); 

        // T is generic, WebServiceWRG in this example 
        Channel     = new ChannelFactory<T>(HttpBinding, Endpoint).CreateChannel();

        UserAccount = new User();
    }

    // another constructor with BasicHttpBinding
    public CallWebService<T>(string url, BasicHttpSecurityMode securityMode)
    {
        Client      = new WebServiceWRGClient(); 

        //See which Binding is the default and use it in this constructor. 
        HttpBinding = new BasicHttpBinding(securityMode);

        Endpoint    = new EndpointAddress(url); 

        // T is generic, WebServiceWRG in this example 
        Channel     = new ChannelFactory<T>(HttpBinding, Endpoint).CreateChannel();

        UserAccount = new User();

    }

    // Change this method to return the response. Task<Response> is just a placeholder for this example 
    public async Task<Response> Call(string userName, string password)
    {
        UserAccount.UserName = await Channel.EncryptValueAsync(userName);

        UserAccount.Password = await Channel.EncryptValueAsync(password);

        var response = await Client.ClaimSearchAsync(User, "", "", 12345, statuscode.NotSet, "");       
    }

    /*
        [To-Do] : gather all other releated methods into this class, then try to simplify them. 

    */

}

Вы также можете настроить конструкторы по мере необходимости, например, вы можете создать конструкторы, которые принимают WebServiceWRGClient и BasicHttpBinding ..и т.д. Таким образом, это более открыто для вас.

Вы можете использовать аналогичный подход, если он будет использоваться во всем проекте, но если он используется только в одном классе, вы можете сделать что-то вроде этого:

// Configure it as needed, but avoid using `void` with async, as the exceptions in sync and async methods handled differently. 
// Also, try to make sense here, make the method return the results.
public async Task CallWebService<T>(WebServiceWRGClient client,  string url, string userName, string password) 
{

    var channelFactory = new ChannelFactory<T>(new BasicHttpBinding(BasicHttpSecurityMode.Transport, new EndpointAddress(url)).CreateChannel(); 
    var user = new User(); // coming from service reference

    user.UserName = await channelFactory.EncryptValueAsync(userName);
    user.Password = await channelFactory.EncryptValueAsync(password);
    var response  = await client.ClaimSearchAsync(user, "", "", 12345, statuscode.NotSet, "");
}
person iSR5    schedule 24.09.2019
comment
Итак, ваша вторая идея заключается в том, где я нахожусь, только с одним классом, но здесь вы не правы и где мне нужна помощь. Класс User отличается для каждой из разных служб, поэтому его нужно передать в метод, и вы пытаетесь передать WebServiceWRGClient, который также зависит от службы (например, WebServiceWRGClient, WebServiceAMICClient, WebServiceFCCClient), чтобы тоже должен быть универсальным. - person user1186050; 26.09.2019
comment
@ user1186050, используйте класс и реализуйте его как CallWebService‹T1, T2, T3›, добавьте столько T, сколько вы хотите передать в класс. Вы можете переименовать букву T во что-то осмысленное, например TWRGClient, TUser, T...etc. - person iSR5; 26.09.2019