3
\$\begingroup\$

I would like feedback on this class library, mainly the architecture. It works very well. But maybe someone knows how to improve it.

This is a class library for working towards an external system. I reuse this class library in multiple solutions. The scope of this review is limited to "Retrieve the customer".

Since this is Xamarin I can't use the proxy class because it takes ages to load on a mobile device (related question here). Which is why I do the requests manually.

Project structure:

enter image description here

The main interface for the class library is the Facade class. It will call on the proper services to retrieve the data that the application requests.

public class Facade
{
    WebRequester WebRequester { get; }

    public Facade(string endpointUrl, string apiUser, string apiPassword)
    {
        WebRequester = new WebRequester(endpointUrl, apiUser, apiPassword);
    }

    /// <exception cref="SoapRequestException"></exception>
    /// <exception cref="System.Net.WebException"></exception>
    public Customer RetrieveCustomer(string customerId)
    {
        return new EntityService(WebRequester).RetrieveCustomer(customerId);
    }
}

BaseService. The WebRequester is injected into the base service.

abstract class BaseService
{
    protected WebRequester WebRequester { get; }

    public BaseService(WebRequester webRequester)
    {
        WebRequester = webRequester;
    }
}

Here's EntityService:

class EntityService : BaseService
{
    public EntityService(WebRequester webRequester) : base(webRequester)
    {
    }

    /// <exception cref="Exceptions.SoapRequestException">Thrown if the response has a Fault.</exception>
    /// <exception cref="System.Net.WebException">Thrown if the request timed out.</exception>
    IEnumerable<XElement> Query(string esql)
    {
        // Prepare request parameters.

        var soapAction = "url-to-soap-action";
        var method = "Query";
        var parameters = new Dictionary<string, string>
        {
            { "Esql", esql }
        };

        // Call the service.

        var result = WebRequester.Invoke(soapAction, method, parameters);
        return result.Descendants(nsTf + "Data")?.FirstOrDefault()?.Elements(nsEm + "Record");
    }

    /// <exception cref="Exceptions.SoapRequestException"></exception>
    /// <exception cref="System.Net.WebException"></exception>
    public Customer RetrieveCustomer(string customerId)
    {
        var esql = $@"
select Id, Email, Name
from Customer
where Id = '{customerId}'";

        var row = Query(esql).FirstOrDefault();
        var serializer = new XmlSerializer(typeof(Customer));
        return serializer.Deserialize(row.CreateReader()) as Customer;
    }
}

On the bottom is WebRequester. Couldn't find a better name. It makes the request to the external API and returns the SOAP response as an XDocument. Also checks for faults.

class WebRequester
{
    string EndpointUrl { get; }
    string ApiUser { get; }
    string ApiPassword { get; }

    public WebRequester(string endpointUrl, string apiUser, string apiPassword)
    {
        EndpointUrl = endpointUrl;
        ApiUser = apiUser;
        ApiPassword = apiPassword;
    }

    static string SessionId { get; set; }

    /// <exception cref="SoapRequestException"></exception>
    /// <exception cref="WebException"></exception>
    string RetrieveSessionId() => new SecurityService(this).BeginSession(ApiUser, ApiPassword);

    /// <exception cref="VerifyLoginFaultException"></exception>
    /// <exception cref="SoapRequestException">Thrown if the SOAP response has a Fault.</exception>
    /// <exception cref="WebException">Thrown if the request times out.</exception>
    public XDocument Invoke(string soapAction, string method, Dictionary<string, string> parameters, bool requireSession = true)
    {
        if (requireSession && SessionId == null)
        {
            SessionId = RetrieveSessionId();
        }

        try
        {
            return Invoke(soapAction, method, parameters, SessionId);
        }
        catch (SessionRequiredException)
        {
            SessionId = RetrieveSessionId();
            return Invoke(soapAction, method, parameters, true);
        }
    }

    /// <exception cref="VerifyLoginFaultException"></exception>
    /// <exception cref="SessionRequiredException"></exception>
    /// <exception cref="SoapRequestException">Thrown if the SOAP response has a Fault.</exception>
    /// <exception cref="WebException">Thrown if the request times out.</exception>
    XDocument Invoke(string soapAction, string method, Dictionary<string, string> parameters, string sessionId)
    {
        // Create the web request

        var request = WebRequest.Create(new Uri(EndpointUrl)) as HttpWebRequest;
        request.Method = "POST";
        request.ContentType = "application/soap+xml; charset=utf-8; action=\"" + soapAction + "\"";
        request.Timeout = 10000;

        // Prepare the session value.

        var session = string.Empty;
        if (sessionId != null)
        {
            session =
$@"<soap:Header>
    <ns1:Session>
        <ns1:SessionId>{sessionId}</ns1:SessionId>
    </ns1:Session>
</soap:Header>";
        }

        // Define which namespace to use for the request body.

        string bodyNs;

        if (method == "Query")
            bodyNs = "ns1";
        else
            bodyNs = "ns2";

        // Prepare the parameters.

        var postValues = new StringBuilder();
        foreach (var param in parameters)
        {
            postValues.AppendLine($"<{bodyNs}:{param.Key}>{param.Value}</{bodyNs}:{param.Key}>");
        }

        // Prepare the SOAP envelope.

        string soapEnvelope =
$@"<?xml version=""1.0"" encoding=""utf-8""?>
<soap:Envelope
    xmlns:soap=""{Constants.NsSoap}""
    xmlns:ns1=""{Constants.NsBase}""
    xmlns:ns2=""{Constants.Ns2008}"">
    {session}
    <soap:Body>
        <{bodyNs}:{method}>
            {postValues.ToString()}
        </{bodyNs}:{method}>
    </soap:Body>
</soap:Envelope>";

        // Create a byte array of the data we want to send.

        byte[] byteData = Encoding.UTF8.GetBytes(soapEnvelope.ToString());

        // Set the content length in the request headers.

        request.ContentLength = byteData.Length;

        // Write data to request.

        using (var postStream = request.GetRequestStream())
        {
            postStream.Write(byteData, 0, byteData.Length);
        }

        // Get response.

        using (var responseReader = new StreamReader(request.GetResponse().GetResponseStream()))
        {
            var stringResult = responseReader.ReadToEnd();
            var result = XDocument.Parse(HttpUtility.HtmlDecode(stringResult));
            var nsSoap = Constants.NsSoap;

            // Look for the <Fault> element in the response.

            var fault = result?.Root?.Element(nsSoap + "Body")?.Element(nsSoap + "Fault");
            if (fault != null)
            {
                // Get the error code value and remove the namespace.
                // Example value: s:InvalidUsernameAndPasswordPair

                var codeWithNs = fault.Element(nsSoap + "Code").Element(nsSoap + "Value").Value;
                var codeSplit = codeWithNs.Split(':');
                var code = codeSplit.Length == 2 ? codeSplit[1] : codeSplit[0];

                if (code == "SessionRequired")
                {
                    throw new SessionRequiredException();
                }
                else if (code == "InvalidUsernameAndPasswordPair")
                {
                    throw new VerifyLoginFaultException();
                }
                else
                {
                    throw new SoapRequestException(code);
                }
            }

            return result;
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ Please post complete classes and not only some snippets so one can also see if they are derived and what fields/properties they have? \$\endgroup\$ – t3chb0t May 1 '18 at 14:56
-1
\$\begingroup\$

I've just the same approach more than once. Sometimes you just have to work with bad designed soap apis. You should refactor the large Invoke method in WebRequester. It's way to long and should be devided in separate methods for building the request and parsing the response.

But all and all I thin it's a okay solution. The downside is that it's harder to update if the api changes.

\$\endgroup\$
  • \$\begingroup\$ This would be a more useful answer if you gave some concrete suggestions besides "refactor the large Invoke method" \$\endgroup\$ – Dannnno May 7 '18 at 13:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.