Firstly, congrats on trying to get a better handle on these concepts. Properly organized code is an art-form and multi-threaded software (particularly debugging it) is wizardry that few understand and even fewer do well (myself included).
That being said, there are a couple of things I would recommend you tweak in your code.
1 -- Avoid using the Thread
type directly if possible
The short story behind this is as I mentioned above. Working with threading in a modern, managed language like .NET is not for the faint of heart and can lead to excruciatingly difficult to diagnose bugs (not the least of which are race conditions, invalid object handles, and memory leaks just to name a few). Microsoft understands this complexity and to help combat, they have created many higher level abstractions that tend to help avoid some of these more common problems. You can read about the abstractions here: https://msdn.microsoft.com/en-us/library/hh156548(v=vs.110).aspx
In this case, I would suggest you use the TPL or (Task Parallel Library).
Here is a direct quote from the article on the TPL:
The Task Parallel Library (TPL) is based on the concept of a task, which represents an asynchronous operation. In some ways, a task resembles a thread or ThreadPool work item, but at a higher level of abstraction. The term task parallelism refers to one or more independent tasks running concurrently. Tasks provide two primary benefits:
For both of these reasons, in the .NET Framework, TPL is the preferred API for writing multi-threaded, asynchronous, and parallel code.
Please have a look here for some really good information on idea behind the TPL and how to implement it: https://msdn.microsoft.com/en-us/library/dd537609(v=vs.110).aspx
2 -- Prefer usings over manual closing of unmanaged resources
The code here:
// Get response from Webserver
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) {
Stream responseStream = response.GetResponseStream();
string responseStr = new StreamReader(responseStream).ReadToEnd();
Console.WriteLine(responseStr + Environment.NewLine);
responseStream.Close();
}
Can be re-written:
string responseStr = string.Empty;
// Get response from Webserver
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) {
using (var responseStream = new StreamReader(response.GetResponseStream())) {
responseStr = responseStream.ReadToEnd();
}
}
Console.WriteLine("{0}{1}", responseStr, Environment.NewLine);
This provides several benefits:
- You can / should want to close the stream ASAP. So doing things like writing to the console and allocating variables are things that can be done elsewhere.
- The nested using is the preferred "best-practice"
Further reading:
3 -- SOLID
Implementing S.O.L.I.D. coding practices in your design will dramatically improve the re-usability, test-ability, and flexibility of your code. In particular, making sure each separate "task" has a well-defined encapsulation that separates that work from other work. In my final example, I will illustrate my application of some of these principles.
For more info on S.O.L.I.D., please see some of the following resources:
Example Code
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
namespace XMLSender {
public class Program {
public static void Main(string[] args) {
string targetServer = string.Empty;
List<Task> tasks = new List<Task>();
Collection<XmlUploadRequest> requests = new Collection<XmlUploadRequest>();
targetServer = LoadTargetServer();
requests = PromptForInputFiles();
// Apply the target server to all requests:
requests.ToList().ForEach(r => r.TargetServer = targetServer);
// Build a task for each request
foreach (var request in requests) {
Task newTask = new Task((x) => UploadWorker.ExecuteWork(x as XmlUploadRequest), request);
tasks.Add(newTask);
}
// the call to .Start will schedule the task using the DefaultTaskScheduler instance
foreach (var task in tasks) {
task.Start();
}
Task.WaitAll(tasks.ToArray()); // Wait for all tasks to complete.
Console.WriteLine("Requests complete.");
Console.ReadLine();
}
internal static string LoadTargetServer() {
Console.WriteLine("Please enter the URL to send the XML File");
return Console.ReadLine();
}
internal static Collection<XmlUploadRequest> PromptForInputFiles() {
List<string> xmlFiles = new List<string>();
string currentFile = string.Empty;
do {
Console.WriteLine("Please enter the XML File you Wish to send, To start simulation type 'start'");
currentFile = Console.ReadLine();
if (currentFile != "start") {
xmlFiles.Add(currentFile);
}
}
while (currentFile != "start");
Collection<XmlUploadRequest> requests = BuildRequests(xmlFiles.ToArray());
return requests;
}
internal static Collection<XmlUploadRequest> BuildRequests(string[] xmlFiles) {
Collection<XmlUploadRequest> requests = new Collection<UserQuery.XmlUploadRequest>();
FileProvider provider = new FileProvider();
if (xmlFiles == null || !xmlFiles.Any()) {
throw new ArgumentException("No xml files provided.", nameof(xmlFiles));
}
foreach (var file in xmlFiles) {
try {
string contents = provider.LoadFileContents(file);
XmlUploadRequest request = new XmlUploadRequest() {
HttpAction = "POST",
UploadContents = contents
};
} catch (FileLoadException fle) {
// Catch our custom type and continue attempting to load the files in order.
Console.WriteLine("{0}: {1}", fle.Message, fle.FileName);
}
}
return requests;
}
}
public static class UploadWorker {
static UploadWorker() {
// Set the connection limit of HTTP
System.Net.ServicePointManager.DefaultConnectionLimit = 10000;
}
public static void ExecuteWork(XmlUploadRequest xmlRequest) {
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(xmlRequest.TargetServer);
int contentLength = xmlRequest.ContentLength();
request.ContentType = "text/xml; encoding=utf-8";
request.ContentLength = contentLength;
request.Method = xmlRequest.HttpAction;
request.KeepAlive = false;
// Closing of the request stream isn't necessary because the using will handle the safe disposal of the unmanaged resources
using (var stream = request.GetRequestStream()) {
stream.Write(System.Text.Encoding.UTF8.GetBytes(xmlRequest.UploadContents), 0, contentLength);
}
string responseStr = string.Empty;
// Get response from Webserver
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) {
using (var responseStream = new StreamReader(response.GetResponseStream())) {
responseStr = responseStream.ReadToEnd();
}
}
Console.WriteLine("{0}{1}", responseStr, Environment.NewLine);
}
}
/// <summary>
/// A type used to pass request data and customized request settings for an XML file upload.
/// This type can be extended or have additional properties added to it to support customizing
/// various aspects of an HTTP request, such as custom headers or request timeout.
/// </summary>
public class XmlUploadRequest {
public XmlUploadRequest() {
this.UploadContents = "<content>empty</content>"; // meaningful defaults
this.HttpAction = "POST"; // meaningful defaults
this.TargetServer = "http://test.com"; // meaningful defaults
}
public virtual int ContentLength() {
return System.Text.Encoding.UTF8.GetBytes(this.UploadContents).GetLength(0);
}
public string UploadContents { get; set; }
public string HttpAction { get; set; }
public string TargetServer { get; set; }
}
public class FileProvider {
private Encoding _fileEncoding;
public FileProvider() : this(Encoding.UTF8) {
}
public FileProvider(Encoding enc) {
this._fileEncoding = enc;
}
public string LoadFileContents(string filePath) {
if (string.IsNullOrWhiteSpace(filePath)) {
throw new ArgumentNullException(nameof(filePath), "No file specified to load.");
}
// Note, this is a "best-effort" check, we know that the nature of the file system is that the file could be present when the call to exists happens and not when we go to read (or vice-versa).
// We still want a pre-emtive check
if (!File.Exists(filePath)) {
throw new FileLoadException("Unable to find the XML file specified.", filePath);
}
string returnValue = string.Empty;
try {
returnValue = File.ReadAllText(filePath);
} catch (IOException ioe) {
Console.WriteLine("Unable to read the file {0}.\n\nReason:{1}\n{2}", filePath, ioe.Message, ioe.StackTrace);
}
return returnValue;
}
} // end class FileProvider
public class FileLoadException : FileNotFoundException {
public FileLoadException(string message) : base(message) {
}
public FileLoadException(string message, string fileName) : base(message, fileName) {
}
public FileLoadException(string message, Exception innerException) : base(message, innerException) {
}
public FileLoadException(string message, string fileName, Exception innerException) : base(message, fileName, innerException) {
}
}
}
Example Explanation
1 Program
class changes
I broke your Program
class out into several encapsulated static methods that each have a very specific sub-task they perform. This modular design has several benefits:
- It breaks the code up into logic blocks that are a bit easier to read.
- The methods themselves can be tested individually
- The design allows for the encapsulated methods to be called from various places as the program increases in complexity.
- It helps to isolate change and reduce code-duplication
Your Program
class now also has but one responsibility: Gather user input. All other responsibilities (such as making HTTP calls, file IO, etc.) are all handled elsewhere.
I have also change the Program
class to use the TPL instead of using threads directly. In this way, we don't need to worry about potential issues with joining and debugging is much nicer.
2 'FileLoadException' type
I introduced a FileLoadException
custom exception type that derives from the System FileNotFoundException
type. I did this mostly to demonstrate isolated exception handling and to allow for possible extension of the type for use in other ways (such as detecting whether or not an XML file is valid, etc...)
If the program encounters this type, it will simply continue processing the other specified files, but any other exception type will be un-handled and force the application to exit.
For more on how to properly handle exceptions, Eric Lippert has some EXCELLENT stuff: https://blogs.msdn.microsoft.com/ericlippert/2008/09/10/vexing-exceptions/
3 SRP and the XmlUploadRequest
type
Separating concerns is in my opinion the HARDEST part of good OO programming. Many would say that naming things is the hardest part, but I say that naming things is a function of defining their responsibility and separating the concerns.
In this case, I opted to create the XmlUploadRequest
type to be a "Unit-Of-Work" model that has all of the information the UploadWorker
type needs to perform it's job. The XmlUploadRequest
is also flexible (as is the UploadWorker
) because we have isolated the responsibility of how / where to load content. It could be in the future that you want to load XML information from a database. If that were to happen, you would need to modify exactly 0% of the code in the UploadWorker
or the XmlUploadRequest
.
One of the benefits of the XmlUploadRequest
type is that it is simple, doesn't have any complex logic and is only concerned about what the class name indicates it is concerned with:
What data is needed to upload XML?
As such, the only modifications that need to happen (see the Open Closed Principle) are those that fix bugs. But you could create an ExtendedTimeoutXmlUploadRequest
type that inherits from XmlUploadRequest
that specifies additional information that can be used by the UploadWorker
type.
This feature means your code-base is STABLE because you can trust that the existing code will not be modified as a regular work item. This means as a developer you spend less time fighting fires and more time adding features.
4 SRP and the FileProvider
type
Normally for something like this I would actually create an interface like IDataProvider
and use that in the static BuildRequests
method in the Program
class. That way I could either give it a DatabaseProvider or a FileProvider but for brevity and simplicity, I left that out (if you like, I can include more detail regarding this).
This class' sole responsibility is to load XML data from the file system. Also notice that I load the data up-front, so there are fewer reasons for the Task to fail. By front-loading the parsing of XML files, I can perform the "mise en place" of programming and separate operations into logical "batches". This is a key feature of OO programming that allows common functionality to be grouped. In your initial example (which worked fine), the upload process did file at a time. If the file is deleted, or the upload takes abnormally long, it's more efficient to be able to immediately begin processing the next item as well as cleanly separates the responsibilities of the code, thereby making them more modular.
Miscellaneous items
Overall everything you had wasn't bad at all and there are near INFINITE ways in which you can "organize" your types. In the end it all comes down to what your intentions are, who else will work on / maintain the code, how long the code will be in production, and many other factors.
Setting the ServicePointManager.DefaultConnectionLimit
is to set a property on a static instance that affects any and all threads in the current AppDomain. I moved this call to a shared constructor, but you could put it anywhere you want as long as you understand that it only needs to be called once. In your example you were calling it for every request. While this isn't necessarily a problem, it is definitely unnecessary and not the expected usage of the property.