Converting formdata NameValueCollection to object

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>();
}

只要nvc中的值实际上与Event类中的属性相匹配,就应该可以工作:

private Event nvcToCoreEvent(NameValueCollection nvc) {
    Event e = new Event();
    foreach (string kvp in nvc.AllKeys) {
        string[] keys = kvp.Substring(0, kvp.Length - 1).Replace("coreEvent[", "").split(new string[] { "][" });
        PropertyInfo pi = e.GetType().GetProperty(keys[0], BindingFlags.Public | BindingFlags.Instance);
        for (int i = 1; i < keys.Length; i++)
            pi = pi.PropertyType.GetProperty(keys[i], BindingFlags.Public | BindingFlags.Instance);

        if (pi != null) {
            pi.SetValue(e, nvc[kvp], null);
        }
    }
    return e;
}
链接地址: http://www.djcxy.com/p/32990.html

上一篇: 使用python阅读Amibroker价格数据

下一篇: 将formdata NameValueCollection转换为对象