I have a WebAPI controller that accepts posts in multipart form data format. It does this as I'm getting both binary files and form data in the same post.
I tried adapting the solution from this question: How do I automap namevaluecollection to a strongly typed class?
Here's the adapted code:
private Event nvcToCoreEvent(NameValueCollection nvc) {
Event e = new Event();
foreach (string kvp in nvc.AllKeys) {
PropertyInfo pi = e.GetType().GetProperty(kvp, BindingFlags.Public | BindingFlags.Instance);
if (pi != null) {
pi.SetValue(e, nvc[kvp], null);
}
}
return e;
}
The problem is the keys coming from nvc.AllKeys
all look like this: "coreEvent[EventId]"
, or this: "coreEvent[AuthorUser][UserId]"
pi
is always null and nothing gets mapped since GetProperty
expects to be passed "EventId"
, not "coreEvent[EventId]"
. If there were just a few properties it wouldn't be so bad but my Event
class is very big and contains sub-objects which contain their own sub-objects, etc. It also contains many lists of objects which can also have their own sub-objects.
Is there any alternative to me writing a key string parser and mapping engine to map the values to the correct sub-object or collection?
EDIT Here's the requested class and sample data:
Event Class
public class Event {
public Event() {
Documents = new List<Document>();
SignOffs = new List<SignOff>();
CrossReferences = new List<CrossReference>();
Notes = new List<Note>();
HistoryLogs = new List<HistoryLog>();
}
public int EventId { get; set; }
public string EventTitle { get; set; }
public User AuthorUser { get; set; }
public User RMUser { get; set; }
public User PublisherUser { get; set; }
public User MoPUser { get; set; }
public EventStatus EventStatus { get; set; }
public WorkPath WorkPath { get; set; }
public Stage Stage { get; set; }
public string EventSummary { get; set; }
public User EventSummaryLastModifiedByUser { get; set; }
public DateTime? EventSummaryLastModifiedOnDate { get; set; }
public Priority Priority { get; set; }
public DateTime? DesiredPublicationDate { get; set; }
public DateTime? DesiredEffectiveDate { get; set; }
public string EffectiveDateReason { get; set; }
public DateTime? AssessmentTargetDate { get; set; }
public DateTime? AssessmentActualDate { get; set; }
public DateTime? SMTargetDate { get; set; }
public DateTime? SMActualDate { get; set; }
public DateTime? FRSOTargetDate { get; set; }
public DateTime? FRSOActualDate { get; set; }
public DateTime? BLRTargetDate { get; set; }
public DateTime? BLRActualDate { get; set; }
public DateTime? SSOTargetDate { get; set; }
public DateTime? SSOActualDate { get; set; }
public DateTime? BLSOTargetDate { get; set; }
public DateTime? BLSOActualDate { get; set; }
public DateTime? FSOTargetDate { get; set; }
public DateTime? FSOActualDate { get; set; }
public DateTime? PublicationTargetDate { get; set; }
public DateTime? PublicationActualDate { get; set; }
public DateTime? EffectiveTargetDate { get; set; }
public DateTime? EffectiveActualDate { get; set; }
public User EffectiveDateReasonLastModifiedByUser { get; set; }
public DateTime? EffectiveDateReasonLastModifiedOnDate { get; set; }
public DateTime? CancellationDate { get; set; }
public string CancellationReason { get; set; }
public DateTime? OnHoldEnteredDate { get; set; }
public DateTime? OnHoldReminderDate { get; set; }
public bool TranslationRequired { get; set; }
public string NewsItemNumber { get; set; }
public string PublicationIdNumber { get; set; }
public IList<Document> Documents { get; set; }
public IList<SignOff> SignOffs { get; set; }
public IList<CrossReference> CrossReferences { get; set; }
public IList<Note> Notes { get; set; }
public IList<HistoryLog> HistoryLogs { get; set; }
public SaveType SaveType { get; set; }
public Stage DestinationStage { get; set; }
}
Here's some samples of the keys in the NameValueCollection
.
A key including an index into a collection:
coreEvent[Documents][0][UploadedByUser][Team][Department][Division][DivisionName]
List within a list:
coreEvent[SignOffs][1][Comments][0][LastModifiedByUser][Team][Department][Division][DivisionName]
This last one should read as coreEvent
is an object containing a list of SignOff
objects each of which contains a list of Comment
objects each of which contains a User
object which contains a Team
object which contains a Department
object which contains a Division
object which contains a string property called DivisionName
.
Well most likely you will have to implement that yourself, but I'll give you a start:
class NameValueCollectionMapper<T> where T : new() {
private static readonly Regex _regex = new Regex(@"\[(?<value>.*?)\]", RegexOptions.Compiled | RegexOptions.Singleline);
public static T Map(NameValueCollection nvc, string rootObjectName) {
var result = new T();
foreach (string kvp in nvc.AllKeys) {
if (!kvp.StartsWith(rootObjectName))
throw new Exception("All keys should start with " + rootObjectName);
var match = _regex.Match(kvp.Remove(0, rootObjectName.Length));
if (match.Success) {
// build path in a form of [Documents, 0, DocumentID]-like array
var path = new List<string>();
while (match.Success) {
path.Add(match.Groups["value"].Value);
match = match.NextMatch();
}
// this is object we currently working on
object currentObject = result;
for (int i = 0; i < path.Count; i++) {
bool last = i == path.Count - 1;
var propName = path[i];
int index;
if (int.TryParse(propName, out index)) {
// index access, like [0]
var list = currentObject as IList;
if (list == null)
throw new Exception("Invalid index access expression"); // more info here
// get the type of item in that list (i.e. Document)
var args = list.GetType().GetGenericArguments();
var listItemType = args[0];
if (last)
{
// may need more sophisticated conversion from string to target type
list[index] = Convert.ChangeType(nvc[kvp], Nullable.GetUnderlyingType(listItemType) ?? listItemType);
}
else
{
// if not initialized - initalize
var next = index < list.Count ? list[index] : null;
if (next == null)
{
// handle IList case in a special way here, since you cannot create instance of interface
next = Activator.CreateInstance(listItemType);
// fill with nulls if not enough items yet
while (index >= list.Count) {
list.Add(null);
}
list[index] = next;
}
currentObject = next;
}
}
else {
var prop = currentObject.GetType().GetProperty(propName, BindingFlags.Instance | BindingFlags.Public);
if (last) {
// may need more sophisticated conversion from string to target type
prop.SetValue(currentObject, Convert.ChangeType(nvc[kvp], Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType));
}
else {
// if not initialized - initalize
var next = prop.GetValue(currentObject);
if (next == null) {
// TODO: handle IList case in a special way here, since you cannot create instance of interface
next = Activator.CreateInstance(prop.PropertyType);
prop.SetValue(currentObject, next);
}
currentObject = next;
}
}
}
}
}
return result;
}
}
Test case:
var nvc = new NameValueCollection();
nvc.Add("coreEvent[EventId]", "1");
nvc.Add("coreEvent[EventTitle]", "title");
nvc.Add("coreEvent[EventDate]", "2012-02-02");
nvc.Add("coreEvent[EventBool]", "True");
nvc.Add("coreEvent[Document][DocumentID]", "1");
nvc.Add("coreEvent[Document][DocumentTitle]", "Document Title");
nvc.Add("coreEvent[Documents][0][DocumentID]", "1");
nvc.Add("coreEvent[Documents][1][DocumentID]", "2");
nvc.Add("coreEvent[Documents][2][DocumentID]", "3");
var ev = NameValueCollectionMapper<Event>.Map(nvc, "coreEvent");
where
public class Event
{
public Event() {
Documents = new List<Document>();
}
public int EventId { get; set; }
public string EventTitle { get; set; }
public DateTime? EventDate { get; set; }
public bool EventBool { get; set; }
public IList<Document> Documents { get; set; }
public Document Document { get; set; }
}
public class Document {
public int DocumentID { get; set; }
public string DocumentTitle { get; set; }
}
Note that if this method id called very often (like you use it in a web service under heavy load) - you may want to cache PropertyInfo and other reflection objects aggressively, because reflection is (relatively) slow.
I've done similar things in MVC to manually invoke the binding of a model in an action but with Web Api this concept isn't as readily available. To top it off, your key names aren't quite what the standard model binders are expecting.
However, I think we can use some of the built in model binder pieces and a bit of custom code to come up with a simple solution.
private Event nvcToCoreEvent(NameValueCollection nvc)
{
Func<string, string> getBinderKey = delegate (string originalKey)
{
IList<string> keyParts = new List<string>();
// Capture anything between square brackets.
foreach (Match m in Regex.Matches(originalKey, @"(?<=\[)(.*?)(?=\])"))
{
int collectionIndex;
if (int.TryParse(m.Value, out collectionIndex))
{
// Preserve what should be actual indexer calls.
keyParts[keyParts.Count - 1] += "[" + m.Value + "]";
}
else
{
keyParts.Add(m.Value);
}
}
// Format the key the way the default binder expects it.
return string.Join(".", keyParts);
};
// Convert the NameValueCollection to a FormDataCollection so we use it's magic sauce.
FormDataCollection formData = new FormDataCollection(nvc.AllKeys.Select(x => new KeyValuePair<string, string>(getBinderKey(x), nvc[x])));
// Internally this actually uses a model binder to do the mapping work!
return formData.ReadAs<Event>();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With