【问题标题】:Converting formdata NameValueCollection to object将表单数据 NameValueCollection 转换为对象
【发布时间】:2016-04-06 07:50:24
【问题描述】:

我有一个 WebAPI 控制器,它接受多部分表单数据格式的帖子。它这样做是因为我在同一个帖子中同时获取二进制文件和表单数据。

我尝试根据这个问题调整解决方案:How do I automap namevaluecollection to a strongly typed class?

这是修改后的代码:

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

问题是来自nvc.AllKeys 的键都看起来像这样:"coreEvent[EventId]",或者这样:"coreEvent[AuthorUser][UserId]"

pi 始终为 null,并且没有任何内容被映射,因为 GetProperty 期望通过 "EventId",而不是 "coreEvent[EventId]"。如果只有几个属性,它不会那么糟糕,但是我的Event 类非常大,并且包含包含它们自己的子对象的子对象等。它还包含许多对象列表,它们也可以有它们的自己的子对象。

我是否可以编写一个键字符串解析器和映射引擎来将值映射到正确的子对象或集合?

编辑 这是请求的类和示例数据:

事件类

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

这是NameValueCollection 中的一些键示例。

包含集合索引的键: coreEvent[Documents][0][UploadedByUser][Team][Department][Division][DivisionName]

列表中的列表: coreEvent[SignOffs][1][Comments][0][LastModifiedByUser][Team][Department][Division][DivisionName]

最后一个应该读作coreEvent是一个包含SignOff对象列表的对象,每个对象都包含Comment对象列表,每个对象都包含User对象,其中包含Team对象其中包含一个 Department 对象,该对象包含一个 Division 对象,该对象包含一个名为 DivisionName 的字符串属性。

【问题讨论】:

  • 您可以发布示例数据和您的 Event 类吗?
  • @JleruOHeP 添加了示例数据和事件类
  • 听起来您想要做的是创建自己的 IModelBinder 来替换默认的 MVC 。我找到了一个 CodeProject article,它描述了你自己做这件事(一旦你想出了正确的实现)。

标签: c# .net asp.net-web-api


【解决方案1】:

很可能你必须自己实现它,但我会给你一个开始:

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

测试用例:

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");

在哪里

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

请注意,如果此方法 ID 被频繁调用(例如您在负载较重的 Web 服务中使用它) - 您可能希望积极缓存 PropertyInfo 和其他反射对象,因为反射(相对)慢。

【讨论】:

    【解决方案2】:

    我在 MVC 中做过类似的事情来手动调用动作中的模型绑定,但是对于 Web Api,这个概念并不那么容易获得。最重要的是,您的键名并不是标准模型绑定器所期望的。

    但是,我认为我们可以使用一些内置的模型绑定器片段和一些自定义代码来提出一个简单的解决方案。

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

    【讨论】:

      【解决方案3】:

      只要你的 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;
      }
      

      【讨论】:

      • 这不处理深度映射,是吗?
      • 它应该映射到属性的深度,只要名称匹配并且属性被实例化。
      • @SteveHarris 这只会获取第一个属性“EventId”。 keys 数组的长度始终为 1,但应该在 1200 左右。此外,Split 函数在传递字符串数组时需要 StringSplitOptions 参数。我使用StringSplitOptions.None 假设这是您的意图。最后我认为nvc[kvp] 总是返回参数的字符串版本。所以类型检查和转换也是必要的。
      猜你喜欢
      • 2013-08-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-26
      • 2012-10-09
      • 1970-01-01
      • 2019-03-16
      • 1970-01-01
      相关资源
      最近更新 更多