Posting Xml and Json MVC 2 Controller Actions

01.19.12

Problem:

You need to submit Xml or Json via Post/Put to your ASP.net MVC 2 Action. The request body must also bind to your models and validate.

Solution:

Write a custom value provider, this is a factory that handles the mapping of the request to forms dictionary. You just inherit from ValueProviderFactory and handle the request if it is of type “application/json” or “application/xml.”

More Info:

Phil Haack

MSDN

StackOverflow.com

CODE:

Add your custom value providers in Global.asax.cs in the method OnApplicationStarted().

protected override void OnApplicationStarted()
{
AreaRegistration.RegisterAllAreas();

RegisterRoutes(RouteTable.Routes);

ValueProviderFactories.Factories.Add(new JsonValueProviderFactory());
ValueProviderFactories.Factories.Add(new XmlValueProviderFactory());
}

JsonValueProviderFactory.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Web.Mvc;
using System.Web.Script.Serialization;

public class JsonValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
var deserializedJson = GetDeserializedJson(controllerContext);

if (deserializedJson == null) return null;

var backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

AddToBackingStore(backingStore, string.Empty, deserializedJson);

return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
}

private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
{
var dictionary = value as IDictionary<string, object>;
if (dictionary != null)
{
foreach (KeyValuePair<string, object> keyValuePair in dictionary)
AddToBackingStore(backingStore, MakePropertyKey(prefix, keyValuePair.Key), keyValuePair.Value);
}
else
{
var list = value as IList;
if (list != null)
{
for (var index = 0; index < list.Count; ++index)
AddToBackingStore(backingStore, MakeArrayKey(prefix, index), list[index]);
}
else backingStore[prefix] = value;
}
}

private static object GetDeserializedJson(ControllerContext controllerContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
if (!contentType.StartsWith("text/json", StringComparison.OrdinalIgnoreCase) &&
!contentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return null;

var input = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();

if (string.IsNullOrEmpty(input)) return null;

return new JavaScriptSerializer().DeserializeObject(input);
}

private static string MakeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
}

private static string MakePropertyKey(string prefix, string propertyName)
{
if (!string.IsNullOrEmpty(prefix))
return prefix + "." + propertyName;
return propertyName;
}
}

XmlValueProviderFactory.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Web.Mvc;
using System.Xml;
using System.Xml.Linq;

public class XmlValueProviderFactory : ValueProviderFactory
{

public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
var deserializedXml = GetDeserializedXml(controllerContext);

if (deserializedXml == null) return null;

var backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

AddToBackingStore(backingStore, string.Empty, deserializedXml.Root);

return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);

}

private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, XElement xmlDoc)
{
// Check the keys to see if this is an array or an object
var uniqueElements = new List<String>();
var totalElments = 0;
foreach (XElement element in xmlDoc.Elements())
{
if (!uniqueElements.Contains(element.Name.LocalName))
uniqueElements.Add(element.Name.LocalName);
totalElments++;
}

var isArray = (uniqueElements.Count == 1 && totalElments > 1);


// Add the elements to the backing store
var elementCount = 0;
foreach (XElement element in xmlDoc.Elements())
{
if (element.HasElements)
{
if (isArray)
AddToBackingStore(backingStore, MakeArrayKey(prefix, elementCount), element);
else
AddToBackingStore(backingStore, MakePropertyKey(prefix, element.Name.LocalName), element);
}
else
{
backingStore.Add(MakePropertyKey(prefix, element.Name.LocalName), element.Value);
}
elementCount++;
}
}


private static string MakeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
}

private static string MakePropertyKey(string prefix, string propertyName)
{
if (!string.IsNullOrEmpty(prefix))
return prefix + "." + propertyName;
return propertyName;
}

private XDocument GetDeserializedXml(ControllerContext controllerContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
if (!contentType.StartsWith("text/xml", StringComparison.OrdinalIgnoreCase) &&
!contentType.StartsWith("application/xml", StringComparison.OrdinalIgnoreCase))
return null;

XDocument xml;
try
{
var xmlReader = new XmlTextReader(controllerContext.HttpContext.Request.InputStream);
xml = XDocument.Load(xmlReader);
}
catch (Exception)
{
return null;
}

if (xml.FirstNode == null)//no xml.
return null;

return xml;
}
}

SQL Server 2008 and Non-ANSI Joins

04.15.11

Problem:

You are upgrading your old SQL server to be compliant with SQL Server 2008 but you are getting errors on incompatible non-ANSI outer join operators ("*=" or "=*").

Solution:

Replace these outer joins with the following syntax.

Left Join:

SELECT e.*, a.AttatchmentPath, a.FileSize
FROM InboxEmails e, EmailAttachments a
WHERE a.EmailID =* e.EmailID

-- The Fix
SELECT e.*, a.AttatchmentPath, a.FileSize
FROM InboxEmails AS e
LEFT JOIN EmailAttachments AS a
ON a.EmailID = e.EmailID

Right Join:

SELECT e.*, a.AttatchmentPath, a.FileSize
FROM InboxEmails e, EmailAttachments a
WHERE a.EmailID *= e.EmailID

-- The Fix
SELECT e.*, a.AttatchmentPath, a.FileSize
FROM InboxEmails AS e
RIGHT JOIN EmailAttachments AS a
ON a.EmailID = e.EmailID

Gotcha: The old way of doing the joins handles nulls differently. There is some pixy dust in how sql limts the amount of record returned. The new syntax removes rows much earlier in the process eliminating rows with nulls that may have showed up in the older style of joins.

MVC Model Binding Complex Collections Gotcha

09.15.10

Problem:

Despite using proper MVC binding HTML conventions values are not being bound correctly to your list. The list may have the correct number of objects but all the value are set to default.

Solution:

Use properties instead of public variables.

Correct Html Markup.

<div>Id</div>
<div>
    <input id="Products_0__Id"
           name="Products[0].Id"
           type="text"
           value="1" />
</div>
<div>Name</div>
<div>
    <input id="Products_0__Name"
           name="Products[0].Name"
           type="text"
           value="Product 1" />
</div>
<div>Quantity</div>
<div>
    <input id="Products_0__Quantity"
           name="Products[0].Quantity"
           type="text"
           value="2" />
</div>
<div>Unit Price</div>
<div>
    <input id="Products_0__UnitPrice"
           name="Products[0].UnitPrice"
           type="text"
           value="200.00" />
</div>

Incorrect View Model:

public class Basket
{
    public List<Product> Products { get; set; }
    public Totals Totals { get; set;}
}
public class Product
{
    public int Id;
    public string Name;
    public int Quantity;
    public decimal UnitPrice;
}

Correct View Model:

public class Basket
{
    public List<Product> Products { get; set; }
    public Totals Totals { get; set;}
}
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

Reading a String from a Stream

07.02.10

Problem: You need to read some text from a stream of some sort.

Solution: Use StreamReader class , set the postion to 0 and call ReadToEnd().

Code!

 

static string GetStringFromStream(Stream inputStream)
{
var pos = inputStream.Position;
var reader = new StreamReader(inputStream);
inputStream.Position = 0;
var text = reader.ReadToEnd();
inputStream.Position = pos;
return text;
}

Returning Json in ASP.Net MVC 1.0

06.22.10

Problem: You want to return a Json object to your page using ASP.Net MVC 1.0.

Solution: Use JQuery.getJSON and JsonActionResult.

CODE:

View HTML

<ul id="jsonRequestFun">
</ul>
<a href="#" id="requestFun">Json is fun</a>

View Javascript

<script type="text/javascript">
$(function () {
$("#requestFun").click(function () {
$.getJSON("/home/jsonfun", null, function (data) {
$("<li>" + data.Message + "</li>").appendTo("#jsonRequestFun");
$("<li>" + data.Time + "</li>").appendTo("#jsonRequestFun");
});
});
});
</script>

 

Model:

public class JsonData
{
public string Message { get; set; }

public string Time { get; set; }
}

 

Controller Action:

Beware of this little gotcha if you do not pass the AllowGet your requests will be ignored.

public ActionResult JsonFun()
{
var data = new JsonData
{
Message = "Hello World",
Time = DateTime.Now.ToShortTimeString()
};
return Json(data, JsonRequestBehavior.AllowGet);
}

 

To view the request use the network tab inside developer tools(F12) of your favorite  browser.

Salesforce Destination

05.26.10

Problem: You need to submit salesforce sObjects including custom fields to many different organization’s salseforce instances.

Solution: Use the Partner WSDL, it's loosely typed and can be used with all organizations and  whatever customizations they have.

 

Code:

//Base class 
public class SalesforceBase
{
private readonly List<SalesforceCustomField> _customFields = new List<SalesforceCustomField>();
private readonly XmlDocument _document = new XmlDocument();
private readonly List<XmlElement> _elements = new List<XmlElement>();
private readonly string _type;

protected SalesforceBase(string type)
{
_type = type;
}

public string Type
{
get
{
return _type;
}
}

public IEnumerable<SalesforceCustomField> CustomFields
{
get
{
return _customFields;
}
}

public XmlElement[] Any
{
get
{
return _elements.ToArray();
}
}

public void AddCustomField(SalesforceCustomField customField)
{
UpdateElements(customField.FieldName, customField.Value);
_customFields.Add(customField);
}

protected void UpdateElements(string name, string value)
{
var element = _elements.Find(ele => ele.Name == name);
if (element != null)
{
element.InnerText = value;
}
else
{
element = _document.CreateElement(name);
element.InnerText = value;
_elements.Add(element);
}
}
}
//Implement Base
public class SalesforceContact : SalesforceBase
{
public SalesforceContact() : base("Contact") { }

private string _firstName;
public string FirstName
{
get { return _firstName; }
set
{
if (value != null)
{
UpdateElements("FirstName", value);
_firstName = value;
}
}
}
}
//Send It
SForceService.create( new sObject(){Any = contact.Any, type = contact.Type});

SVN Merge Headaches

03.05.10

Problem: You work on a team using svn source repository and you are plagued with merge conflicts.

Solution: Simply put use ignore. All team members need to have the ignore list setup so that only developer generated content is being submitted. User files and compiled project files should be omitted.

In my development environment I use Visual Studio 2008(C# mostly) and Resharper. My automated builds compile to a folder called “build.”

My ignore list:

.DS_Store _ReSharper.* Thumbs.db obj bin *.suo build *.user buildout.txt

User Generated Html Content

10.22.09

Problem: You need to handle user generated html content from an unknown source for display in your webapp.

Solution: Well this is alwasy a moving target due to cross site scripting, but my approach is the following.

1. I use MarkItUp to allow users an easy way to format their html.

2. After users has submitted his changes I run it through an HTML Sanitizer (Scroll to the bottom) that users a white list approach. For asp.net mvc you will need to mark your controller as [ValidateInput(false)]

2. If the Sanitization process has removed any user created content I do not save the content.  I then Return there modified content with a warning message, "Some illegal content tags where detected and removed double check your work and try again."  

3. If the content passes through the sanitization process cleanly, I save the raw html content to the database.

4. When rendering to the client I just pass the raw html out of the db to the page.

More Info:

StackOverflow.com

Safer Deletes and Updates on Live DataBase

05.21.09

Problem: You need to change production data from a database.

Solution: Use Transations.

Its always scary working with live data I found that I feel more confident when I take the following measures.

Do a Select first to check the where clause:

SELECT * FROM Foo WHERE FooID = 1000

Wrap your changes in a Transaction and roll it back if you did not get the expected result.

Begin Transaction
  DELETE FROM Foo WHERE FooID = 1000
IF @@RowCount <> 1 BEGIN
  Rollback Transaction
END
ELSE
  Commit Transaction

Visual Studio Throws an Error When Trying to Connect/Open an .mdf File

04.11.09

Problem: You try to open a .mdf file in visual studio and you get one of the following errors.

Connections to SQL Server files (*.mdf) require SQL Server Express 2005 to function properly.  Please verify the installation of the component or download from the URL:  http://go.microsoft.com/fwlink/?LinkId=49251

Failed to generate a user instance of SQL Server due to a failure in starting the process for the user instance. The connection will be closed.

Solution: Open Server Explorer add Data Connection. Make sure the Data Source = Microsoft SQL Server Database File (SqlClient) and browse to the .mdf file located in your source directory(There maybe on located in the bin directory but this will get copied over by what is in src on a build). Click the Advance button. In order to work I needed to switch the Data Source to .\SQLEXPRESS2005 (This will be different for each computer).

Final connection string should look something like this:

Data Source=.\SQLEXPRESS2005;AttachDbFilename=|DataDirectory|\NerdDinner.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True