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