Код для инициализации .Net-объекта из файла CSV


Я инициализации следующий объект из файла CSV:

private class MassUploadUser
    {
        public string email { get; set; }

        public string firstName { get; set; }

        public string lastName { get; set; }

        public string role { get; set; }

        public bool active { get { return true; } }
    }

Я потом сериализовать это в JSON используя Newtonsoft, а затем Post это API-интерфейс RESTful. TextFieldParser от Microsoft.VisualBasic.FileIO кстати.

using (var parser = new TextFieldParser(Program.file))
{
    parser.SetDelimiters(",");

    string[] header = parser.ReadFields();

    while (!parser.EndOfData)
    {
            string[] fields = parser.ReadFields();

            var massUploadUser = new MassUploadUser();
            string password = null;

            for (int i = 0; i < fields.Length; i++)
            {
                switch (header[i])
                {
                    case "First Name":
                        massUploadUser.firstName = fields[i];
                        break;

                    case "Last Name":
                        massUploadUser.lastName = fields[i];
                        break;

                    case "Role":
                        massUploadUser.role = fields[i];
                        break;

                    case "email":
                        massUploadUser.email = fields[i];
                        break;

                    case "Password":
                        password = fields[i];
                        break;
                }
            }

           // After the for loop, I have some additional logic
           // to serialize the newly-created object to JSON
           // and then POST it to a RESTful API
           DoPostToWebService(massUploadUser, password);

          // At this point, we created the new user on the server, so no need
          // to keep the current instance of massUploadUser around
        }

Это может быть улучшено? Есть более "общие" способ сделать это?



3510
3
задан 20 марта 2018 в 07:03 Источник Поделиться
Комментарии
5 ответов

MassUploadUser

На основе .Чистая принципов именования свойства должны быть названы с использованием PascalCase корпус.

Цикл

В своем нынешнем виде вы делаете для каждого столбца в каждой строке в switch чтобы получить индекс столбца и значение. Извлекать это в отдельный метод, как так

private  Dictionary<string, int> ReadColumnIndexes(string[] headers)
{

return headers.Select((v, i) => new { Key = v, Value = i })
.ToDictionary(o => o.Key, o => o.Value);
}

Вы можете потом, за пределами while петли, назначить нужные столбца индекса переменных, как так

var columnDictionary = ReadColumnIndexes(headers);

var firstNameColumn = columnDictionary["First Name"];
var lastNameColumn = columnDictionary["Last Name"];
.....
var passwordColumn = columnDictionary["Password"];

теперь ваш цикл может выглядеть так

while (!parser.EndOfData)
{
string[] fields = parser.ReadFields();

var massUploadUser = new MassUploadUser();
massUploadUser.firstname = fields[firstNameColumn];
massUploadUser.lastname = fields[lastNameColumn];
.....
string password = fields[passwordColumn];

// After the for loop, I have some additional logic
// to serialize the newly-created object to JSON
// and then POST it to a RESTful API
DoPostToWebService(massUploadUser, password);

// At this point, we created the new user on the server, so no need
// to keep the current instance of massUploadUser around
}

3
ответ дан 21 марта 2018 в 06:03 Источник Поделиться

Вот еще один ответ, используя отражение, Expression и Attribute чтобы создать более автоматизированное решение.

Сначала вам нужно создать несколько пользовательских AttributeС пометить поля или свойства с CSV-файл по названию, когда это не соответствует классу имя, или указать член класса не инициализируется из CSV:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class CSVColumnName : Attribute {
public string ColumnName { get; }
public CSVColumnName(string name) => ColumnName = name;
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class CSVNoColumnName : Attribute {
}

Затем вы можете создать статический метод для создания PropertySetters Dictionary для вас на размышления. Вам нужно несколько методов расширения, чтобы сделать MemberInfo легче работать с:

public static Type GetMemberType(this MemberInfo member) {
switch (member.MemberType) {
case MemberTypes.Field:
return ((FieldInfo)member).FieldType;
case MemberTypes.Property:
return ((PropertyInfo)member).PropertyType;
case MemberTypes.Event:
return ((EventInfo)member).EventHandlerType;
default:
throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", "member");
}
}

public static bool GetCanWrite(this MemberInfo member) {
switch (member.MemberType) {
case MemberTypes.Field:
return true;
case MemberTypes.Property:
return ((PropertyInfo)member).CanWrite;
default:
throw new ArgumentException("MemberInfo must be if type FieldInfo or PropertyInfo", "member");
}
}

Затем вы можете создать сеттер лямбды для каждого из полей и положить их в Dictionary:

public static class CSVMapping<T> {
public static Dictionary<string, Action<T, object>> PropertySetters() {
var t = typeof(T);
var propsOrFields = t.GetMembers(BindingFlags.Instance | BindingFlags.Public).Where(m => m.MemberType == MemberTypes.Property || m.MemberType == MemberTypes.Field);

var ans = new Dictionary<string, Action<T, object>>(StringComparer.OrginalIgnoreCase);
foreach (var m in propsOrFields) {
if (!Attribute.IsDefined(m, typeof(CSVNoColumnName)) && m.GetCanWrite()) {
var ca = (CSVColumnName)Attribute.GetCustomAttribute(m, typeof(CSVColumnName));
var csvname = (ca != null) ? ca.ColumnName : m.Name;
// (T p1, object p2) => p1.{m.Name} = ({m.Type})p2;
var paramobj = Expression.Parameter(t);
var paramval = Expression.Parameter(typeof(object));
var body = Expression.Assign(Expression.PropertyOrField(paramobj, m.Name), Expression.Convert(paramval, m.GetMemberType()));
var setter = Expression.Lambda<Action<T, object>>(body, new[] { paramobj, paramval });
ans.Add(csvname, setter.Compile());
}
}
return ans;
}
}

Примечание: Вы могли бы использовать отражение, чтобы присвоить значения так же, но я думаю, что это стоит усилий, чтобы построить лямбда-выражения, поскольку вы, вероятно, будут назначены на поля часто (раз в CSV строки файла).

Теперь вы можете аннотировать класс с CSV информация:

private class MassUploadUser {
public string email { get; set; }

[CSVColumnName("First Name")]
public string firstName { get; set; }

[CSVColumnName("Last Name")]
public string lastName { get; set; }

public string role { get; set; }

[CSVNoColumnName]
public bool active { get { return true; } }
}

Примечание: с active это свойство только для чтения, в PropertySetters метод был пропустить его, даже если он не имеет атрибута.

Наконец, вы можете преобразовать CSV файл для учеников так же, как мой предыдущий ответ, с помощью сеттеров Dictionary назначение чтения в значениях:

var fieldMap = CSVMapping<MassUploadUser>.PropertySetters();

using (var parser = new TextFieldParser(csvFilename)) {
parser.SetDelimiters(",");

string[] header = parser.ReadFields();
var headerMap = header.Select((h, i) => new { h, i }).ToDictionary(hi => hi.h, hi => hi.i);

while (!parser.EndOfData) {
string[] fields = parser.ReadFields();

var massUploadUser = new MassUploadUser();
foreach (var field in fieldMap.Keys)
fieldMap[field](massUploadUser, fields[headerMap[field]]);
}
}

3
ответ дан 21 марта 2018 в 07:03 Источник Поделиться

Поскольку вы уже знали заголовки, используемые в вашей for петли, затем делегат позволит сократить количество петель необходимо для выполнения повторяющихся задач создания Роко.

string[] headers = parser.ReadFields();
Func<string[], MassUploadUser> factory = (string[] fields) =>
new MassUploadUser {
email = fields[headers.IndexOf("email")],
firstName = fields[headers.IndexOf("First Name")],
lastName = fields[headers.IndexOf("Last Name")],
role = fields[headers.IndexOf("Role")]
};

//...

Что само по себе в данном примере достаточно просто. Но для более сложных данных, может стать очень сложным очень быстро.

Я очень неравнодушен к построению выражений и делегатов для обработки повторяющихся общих задач, которые хорошо поддаются для LINQ. Этот сценарий попадет в эту категорию.

Опираясь на уже представили свои предложения, которые обеспечивают хорошие предложения, лямбда-выражение может быть построено с помощью отражения и конвенций именования для упрощения создания объектов из csv-файла.

Следующие утилиты были построены, чтобы удовлетворить эту. Надеюсь, код и сопроводительной документации говорит само за себя.

public static class ExpressionManager {

/// <summary>
/// Builds an expression that creates a new object and initializes properties from a string array
/// </summary>
public static Expression<Func<string[], T>> BuildFactoryExpressionFor<T>(string[] headers) where T : new() {
var type = typeof(T);
var properties = type.GetCachedProperties();
var columns = MapColumnIndexes(headers);
//Desired delegate
//Func<string[], T> factory = (string[] fields) => new T() { X = fields[0], Y = fields[1] };

// (string[] fields) => ...
var parameter = Expression.Parameter(headers.GetType(), "fields");
// new T()
var newOfType = Expression.New(type);
// { PropertyA = fields[0], PropertyB = (int)fields[1] }
var memberBindings = getMemberBindings(columns, properties, parameter);
// new T() { PropertyA = fields[0], PropertyB = (int)fields[1] };
var body = Expression.MemberInit(newOfType, memberBindings);
// (string[] fields) => new T() { PropertyA = fields[0], PropertyB = (int)fields[1] };
var lambda = Expression.Lambda<Func<string[], T>>(body, parameter);
return lambda;
}
/// <summary>
/// Get the bindings used to populate the provided properties
/// </summary>
private static IEnumerable<MemberAssignment> getMemberBindings(IDictionary<string, int> columns, PropertyInfo[] properties, ParameterExpression parameter) {
using (var e = columns.Keys.GetEnumerator()) {
while (e.MoveNext()) {
var headerName = e.Current;
var propertyName = headerName.Replace(" ", "");//<-- simple naming convention
var propertyInfo = properties.FirstOrDefault(_ => string.Equals(_.Name, propertyName, StringComparison.InvariantCultureIgnoreCase));
if (propertyInfo != null) {
var setMthd = propertyInfo.GetSetMethod(true);
if (propertyInfo.CanWrite && setMthd != null && setMthd.IsPublic) {
var propertyType = propertyInfo.PropertyType;
// index
var headerIndex = Expression.Constant(columns[headerName]);
// fields[index]
Expression value = Expression.ArrayAccess(parameter, headerIndex);
if (propertyType != typeof(string)) {
// (int)Coerce(fields[index], typeof(int))
value = Expression.Convert(Expression.Call(getConverter(), value, Expression.Constant(propertyType)), propertyType);
}
// Property = value
var setter = Expression.Bind(propertyInfo, value);
yield return setter;
}
}
}
}
}

static MethodInfo coerce;
static MethodInfo getConverter() {
if (coerce == null) {
var flags = BindingFlags.Static | BindingFlags.NonPublic;
coerce = typeof(ExpressionManager).GetMethod("CoerceValue", flags);
}
return coerce;
}

static object CoerceValue(object value, Type conversionType) {
if (value == null || (value is string && string.IsNullOrWhiteSpace(value as string))) {
return conversionType.GetDefaultValueForType();
}
//TODO: room for improvement here for other types. consider automapper.
try {
return Convert.ChangeType(value, conversionType,
System.Globalization.CultureInfo.InvariantCulture);
} catch { }
if (isNullable(conversionType)) {
try {
var underlyingType = Nullable.GetUnderlyingType(conversionType);
return Convert.ChangeType(value, underlyingType,
System.Globalization.CultureInfo.InvariantCulture);
} catch { }
}
return conversionType.GetDefaultValueForType();
}

static bool isNullable(Type conversionType) {
return conversionType.IsGenericType &&
conversionType.GetGenericTypeDefinition().IsAssignableFrom(typeof(Nullable<>));
}

static Dictionary<Type, object> defaultValueTypes = new Dictionary<Type, object>();
/// <summary>
/// Gets the default value for a type.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>The default value.</returns>
static object GetDefaultValueForType(this Type type) {
if (!type.IsValueType) return null;

object defaultValue;

if (defaultValueTypes.TryGetValue(type, out defaultValue)) return defaultValue;

defaultValue = type.CreateInstance();

defaultValueTypes[type] = defaultValue;

return defaultValue;
}

public static IDictionary<string, int> MapColumnIndexes(this string[] headers) {
return headers
.Select((header, index) => new { header, index })
.ToDictionary(o => o.header, o => o.index);
}

private static readonly IDictionary<Type, PropertyInfo[]> propertyCache = new Dictionary<Type, PropertyInfo[]>();
/// <summary>
/// Returns all the public properties of the current <seealso cref="System.Type"/>.
/// </summary>
/// <param name="type">The type to get the properties from</param>
/// <returns></returns>
public static PropertyInfo[] GetCachedProperties(this Type type) {
PropertyInfo[] properties = new PropertyInfo[0];
if (!propertyCache.TryGetValue(type, out properties)) {
lock (propertyCache) {
if (!propertyCache.TryGetValue(type, out properties)) {
var flags = BindingFlags.Public | BindingFlags.Instance;
properties = type.GetProperties(flags);
propertyCache[type] = properties;
}
}
}
return properties;
}
}

Мой первоначальный вопрос был в том, чтобы позволить для более гибкий завод делегат, который будет пытаться конвертировать предоставленные строковых значений при сопоставлении с нестроковыми свойства.

//...

if (propertyType != typeof(string)) {
// (int)Coerce(fields[index], typeof(int))
value = Expression.Convert(Expression.Call(getConverter(), value, Expression.Constant(propertyType)), propertyType);
}

//...

Это позволит в собственность, как

public DateTime BirthDate { get; set; }

на карте в столбец CSV, как

"2018-03-21"

и правильно DateTime будет передана на подключенный собственность.

На данный момент, упрощенный CoerceValue метод должен быть в состоянии справиться с простым типом преобразования между типами значений, а также nullables. Есть номер для улучшения здесь в качестве библиотеки, как Automapper может пригодиться.

С помощью простого именования, как удалять пробелы из названия заголовка CSV для сравнения с целевым имена свойств, упрощает один к одному отображению имен заголовка CSV для имен свойств. Опять же это может быть улучшено с помощью метаданных из атрибутов, при желании, как это предложено в другой ответ.

например

"First Name" => firstName
"Last Name" => lastName
...etc

В то время как предоставленные коммунальные услуги могут выглядеть как много под капотом, это позволит более упрощенный способ, когда рефакторинг

using (var parser = new TextFieldParser(Program.file)) {
parser.SetDelimiters(",");

string[] headers = parser.ReadFields();
//delegate to build desired objects
var factory = ExpressionManager.BuildFactoryExpressionFor<MassUploadUser>(headers).Compile();
//Need this to access Password as it is not included in POCO
var passwordColumn = headers.IndexOf("Password");

while (!parser.EndOfData){
string[] fields = parser.ReadFields();
MassUploadUser massUploadUser = factory(fields);
string password = fields[passwordColumn];

// After the for loop, I have some additional logic
// to serialize the newly-created object to JSON
// and then POST it to a RESTful API
DoPostToWebService(massUploadUser, password);

// At this point, we created the new user on the server, so no need
// to keep the current instance of massUploadUser around
}
}

3
ответ дан 22 марта 2018 в 12:03 Источник Поделиться

Я не уверен, если отражения не будет окончательное решение (вы должны были бы использовать пользовательский атрибут, чтобы указать на несоответствие CSV с заголовками), но можно вручную создать карту для имен столбцов для полей MassUploadUser класс:

public readonly static Dictionary<string, Action<MassUploadUser, object>> PropertySetters = new Dictionary<string, Action<MassUploadUser, object>>() {
{ "First Name", (u,v) => u.firstName = (string)v },
{ "Last Name", (u,v) => u.lastName = (string)v },
{ "Role", (u,v) => u.role = (string)v },
{ "email", (u,v) => u.email = (string)v },
};

Затем вы можете создать еще одну карту, чтобы сопоставить имена заголовков столбцов (поля) цифрами и процесс сопоставленные поля:

var fieldMap = MassUploadUser.PropertySetters;

using (var parser = new TextFieldParser(@"")) {
parser.SetDelimiters(",");

string[] header = parser.ReadFields();
var headerMap = header.Select((h, i) => new { h, i }).ToDictionary(hi => hi.h, hi => hi.i);

while (!parser.EndOfData) {
string[] fields = parser.ReadFields();

var massUploadUser = new MassUploadUser();
foreach (var field in fieldMap.Keys)
fieldMap[field](massUploadUser, fields[headerMap[field]]);
}

2
ответ дан 21 марта 2018 в 12:03 Источник Поделиться

Я бы предположил, что есть более эффективные способы, чем использование Microsoft.VisualBasic библиотеки. Это не только потому, что вы используете c#; я дал тот же совет, много раз в прошлом для тех, кто использует VB.NET.

Есть много ссылок о том, как читать текст .Чистая. Вот один из используя System.IO:

https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-read-text-from-a-file

Для разделения полей, есть строка.Сплит способ.

Для чтения отдельных полей, см. ответ от @Heslacher. Обратите внимание на его другие советы, такие как имен и т. д. Говоря о наименовании, MassUploadUser не очень хорошее название. Я бы предложил что-нибудь попроще и более прямые, такие как Userили Personили Employee.

Я отмечаю, что Вы читаете по одной строке за раз, и размещая одну строку за один раз. Вы можете рассмотреть возможность сделать это оптом, в этом случае вы хотели бы List<User> (или List<MassUploadUser>).

0
ответ дан 21 марта 2018 в 12:03 Источник Поделиться