Accept / Reject File Upload Depending Upon XML Schema Validation
Uploading files from the client machine onto the server is a fairly common
task in web applications. Recently I came across such an application where the
end users are required to upload XML files from their machine onto the server.
These XML files were produced as a result of some export operation of a desktop
application installed on their machines. These XML files then used to get
imported in some central database for further processing. Uploading the
file is a quite straightforward thing but in this case it was also required to
validate these uploaded files against an XML schema (XSD). This was a safeguard
against manual or accidental tampering of the files that might take place at the
end user's side. If a file is found to be invalid the it shouldn't be accepted
in the system for obvious reason. This article discusses a simple solution to
accomplish this task.
Have a look at the following figure:
The above page allows you to upload one or more files on the server. Before
the files are saved on the server you check whether the uploaded file(s) is a
valid XML document. You do this by validating the incoming file against an XML
schema. You accept only those files that are valid as per the schema rules. The
success and error messages are displayed on the page to keep the user informed
of the outcome.
To build this sample application, create a new ASP.NET MVC project in Visual
Studio. Add HomeController and Index view as you normally do. Then open the
Index.cshtml file and enter the following markup and code:
@model List<string>
@{
Layout = null;
}
<!DOCTYPE html>
...
<body>
<h1>Upload XML Files</h1>
@using (Html.BeginForm("Upload", "Home",
FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<h3>Select file(s) to upload :</h3>
@Html.TextBox("file", "",
new { type = "file",multiple="multiple" })
<br /><br />
<input type="submit" value="Upload Selected Files" />
<br />
if (Model != null)
{
<ul>
@foreach (string s in Model)
{
<li><strong>@s</strong></li>
}
</ul>
}
}
</body>
</html>
The Index view receives a List of string messages from the controller (if
any) as its model. The Index view basically renders a <form> using the BeginForm()
helper. Notice the parameters of the BeginForm(). The form will be posted to
Upload() action of the HomeController. Also, the enctype of the <form> is set to
multipart/form-data since we wish to upload files.
The file field is rendered using the TextBox() helper. The type attribute is
changed from the default of text to file and its multiple attribute is also set
to allow multiple file selection. A submit button submits the form. Below the
submit button there is a foreach loop they outputs all the success or error
messages.
So far so good. Now let's focus on the more important part - validating XML
file against an XSD schema.
As an example we will use the following XML document :
<?xml version="1.0" encoding="utf-8" ?>
<employees>
<employee employeeid="1">
<firstname>Nancy</firstname>
<lastname>Davolio</lastname>
</employee>
<employee employeeid="2">
<firstname>Andrew</firstname>
<lastname>Fuller</lastname>
</employee>
<employee employeeid="3">
<firstname>Janet</firstname>
<lastname>Leverling</lastname>
</employee>
</employees>
This is a simple XML document with root element of <employees>. Each employee
is wrapped in the <employee> element. The employeeid attribute holds an
employee's ID. The <firstname> and <lastname> hold the employee's First Name and
Last Name respectively. The end user is supposed to upload XML files matching
this structure.
Now we want to ensure that the uploaded files are indeed match this
structure. This validation is done with the help of the following XML Schema :
<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified"
elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="employees">
<xs:complexType>
<xs:sequence>
<xs:element name="employee"
type="EmployeeType" minOccurs="0"
maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="EmployeeType">
<xs:all>
<xs:element name="firstname" type="NameSimpleType" />
<xs:element name="lastname" type="NameSimpleType" />
</xs:all>
<xs:attribute name="employeeid"
type="xs:int" use="required" />
</xs:complexType>
<xs:simpleType name="NameSimpleType">
<xs:restriction base="xs:string">
<xs:minLength value="3" />
<xs:maxLength value="255" />
</xs:restriction>
</xs:simpleType>
</xs:schema>
We won't go into the details of this schema. Just add this schema markup into
Employees.xsd and store it somewhere in the project folder. Also create two
sample XML files - Employees1.xml and Employees2.xml - by pasting the XML markup
shown earlier. In one of the files remove the employeeid so that the validation
will fail as per schema rules.
Ok. Now open the HomeController and add the following code to it.
public class HomeController : Controller
{
private List<string> messages = new List<string>();
private string currentFileName = "";
private bool flag = false;
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Upload()
{
var postedFiles = Request.Files;
for(int i=0;i<postedFiles.Count;i++)
{
HttpPostedFileBase file = postedFiles[i];
currentFileName = Path.GetFileName(file.FileName);
string xmlPath = Server.MapPath
($"~/XmlData/{currentFileName}");
string xsdPath = Server.MapPath
($"~/XmlData/Employees.xsd");
XmlReaderSettings settings = new XmlReaderSettings();
settings.ValidationType = ValidationType.Schema;
settings.Schemas.Add("", xsdPath);
settings.ValidationEventHandler +=
new ValidationEventHandler(OnValidationError);
XmlReader reader = XmlReader.Create
(file.InputStream, settings);
while (reader.Read())
{
}
reader.Close();
if (flag)
{
messages.Add($"Schema validation
failed for {currentFileName}");
//do not save file on the server
}
else
{
file.SaveAs(xmlPath);
messages.Add($"Validation
success for {currentFileName}");
}
flag = false;
}
return View("Index",messages);
}
void OnValidationError(object sender,
ValidationEventArgs e)
{
flag = true;
messages.Add($"ERROR : {currentFileName}
-- {e.Message}");
}
The code declares three private variables inside the HomeController class.
The messages List is intended to store success as well as error messages. These
messages will be shown to the user by outputting them onto the Index view. Why
do we need a class level variable? That's because the validation event handler
also needs access to this List. You will understand this when we discuss the
code further. The currentFileName variable holds the name of the file that is
being validated. This variable, too, is declared at class level for the reason
mentioned earlier. The flag Boolean variable is just used to detect whether a
file is done with the validation or not.
The Index() action is quite straightforward and simple displays the Index
view in the browser.
The <form> is posted to the Upload() method. This is where the XML document
validation and file upload takes place. Let's see how.
The code retrieves a list of files posted to the server using the
Request.Files collection. A for loop iterates through these files one-by-one.
Each file from the Files collection is HttpPostedFileBase object. The FileName
(excluding the client side path) is stored in the currentFileName variable. This
file name is obtained using the GetFileName() method of the Path class.
Then the server side path of the XML file is determined using Server.MapaPath()
method. This is where the uploaded file will be saved if the schema validation
succeeds. In the above example the XML files will be stored in the XmlData
folder under the project root. On the same lines the physical path of
Employees.xsd is determined. This path is needed in the later part of the code.
Then the code creates XmlReaderSettings object. This object contains the
settings to be used while performing the validation. The ValidationType property
is set to Schema. Since we wish to validate the incoming XML documents against
Employees.xsd we add the schema file in the Schemas collection. The Schemas
property is actually XmlSchemaSet collection.
Then the ValidationEventHandler of the XmlReaderSettings is wired to
OnValidationError method. The ValidationEventHandler event is raised during the
validation process whenever there is any validation error. The OnValidationError()
method is discussed later.
Then an XmlReader object is created. Notice the two parameter of the
XmlReader constructor. The first parameter is the InputStream of the incoming
file. Since the file is not yet "accepted" by the system it's not physically
saved onto the server. Hence, we pass the file's InputStream. The second
parameter is the XmlReaderSettings object.
A while() loop runs the XmlReader's Read() method. The Read() method returns
false when the XmlReader reaches the end of the stream. Once the validation is
over the code closes the XmlReader by calling its Close() method.
If the OnValidationError() has set the flag variable to true, it indicates
that the current file has validation errors. If so, we add a failure message in
the List. If the flag is false it means there were no validation errors. We then
save the incoming file using the SaveAs() method of HttpPostedFileBase object.
We also add a success message in the messages List. Once the current file is
over we reset the flag variable to false.
Finally, the Index view is displayed back to the user. This time messages
List is passed to the Index view as its model.
The OnValidationError() event handler receives ValidationEventArgs parameter.
The Message property of ValidationEventArgs tells us what went wrong during the
validation. We add this message and the currentFileName to the messages List.
This completes the example. Run it and pick both the files - Employees1.xml
and Employees2.xml. You should see success and error messages as shown in the
figure earlier.
That's it for now! Keep coding !!