Tuesday, January 12, 2010

Project Server entity mapping: Data Access Layer

In the previous post we introduced our vision of dealing with data on Project Server platform. Now we’ll show main ideas of data mapping. If you are not rather familiar with ProjectDataSet type check out the following link first http://msdn.microsoft.com/en-us/library/websvcproject.projectdataset_members.aspx.

There are two types of fields in Project Server: intrinsic and custom fields. We implemented mapping attributes for both types to get all necessary information for data access. For intrinsic fields we specify name of the column in Project data table. For custom fields data access we use custom field UID to find proper row in ProjectCustomFields table. And for both cases we have a simple data type conversion which updates entity property with loaded data and vice-versa.

So, our Get method implementation from PSProjectService from previous post is following:
public T Get(Guid entityUid)
{
    try
    {
        int dataFlags = (int)ProjectEntityType.Project |
                        (int)ProjectEntityType.ProjectCustomFields;

        using (DataSet dsProject = _wssProject.ReadProjectEntities(
                                       entityUid,
                                       dataFlags,
                                       DataStore))
        {
            if (dsProject != null &&
                dsProject.Tables.Contains(PsDataTableNames.Project) &&
                dsProject.Tables[PsDataTableNames.Project].Rows.Count > 0)
            {
                DataRow projectRow = dsProject.Tables[PsDataTableNames.Project].Rows[0];
                var project = new T();
                var entityDataMapper = new EntityDataMapper<T>(_logService);

                // load intrinsic fields data
                entityDataMapper.MapDataRowToEntityNativeFields(
                    projectRow,
                    project,
                    DataRowVersion.Current);

                // load custom fields data
                entityDataMapper.MapCustomFieldsToEntity(
                    project,
                    entityUid,
                    dsProject.Tables[PsDataTableNames.ProjectCustomFields],
                    DataRowVersion.Current,
                    new ProjectCustomFieldsMappingInfo());

                return project;
            }
        }
    }
    catch (SoapException ex)
    {
        _logService.Log("Read project exception: " + ex.Message);
    }
    return null;
}
Here we are using EntityDataMapper class for mapping data from DataSet to entity. MapDataRowToEntityNativeFields method loads data from the project row:
public T MapDataRowToEntityNativeFields(DataRow row, T entity, DataRowVersion dataRowVersion)
{
    typeof(T).ForeachProperty(propertyInfo =>
    {
        var attr = typeof(T)
                       .GetPropertyCustomAttribute<PSNativeFieldAttribute>(propertyInfo);
        if (attr != null)
        {
            string columnName = attr.ColumnName.Trim();
            if (row.Table.Columns.Contains(columnName))
            {
                try
                {
                    object val = RowDataAdapter.GetRowData(
                                     row,
                                     columnName,
                                     propertyInfo.PropertyType,
                                     dataRowVersion);

                    if (val != null || !propertyInfo.PropertyType.IsValueType)
                        propertyInfo.SetValue(entity, val, null);
                }
                catch (Exception ex)
                {
                    _logService.Error("Native field mapping exception: " + ex.Message);
                    throw;
                }
            }
        }
    });

    return entity;
}
And MapCustomFieldsToEntity method loads custom fields’ data:
internal void MapCustomFieldsToEntity(T entity,
                                      Guid entityUid,
                                      DataTable dtCustomFields,
                                      DataRowVersion version,
                                      ICustomFieldsMappingInfo cfMappingInfo)
{
    var customFields = new Dictionary<Guid, object>();

    foreach (DataRow customFieldRow in dtCustomFields.Rows)
    {
        if (entityUid == RowDataAdapter.GetRowData<Guid>(
                             customFieldRow,
                             cfMappingInfo.EntityUidColumnName,
                             version))
        {
            try
            {
                var propId = RowDataAdapter.GetRowData<Guid>(
                                 customFieldRow,
                                 CustomFieldsColumnNames.MD_PROP_UID,
                                 version);

                object fieldValue = GetCustomFieldRowValue(customFieldRow, version);

                if (!customFields.ContainsKey(propId))
                    customFields.Add(propId, fieldValue);                        

            }
            catch (Exception ex)
            {
                _logService.Error("Custom fields mapping exception: " + ex.Message);
                throw;
            }
        }
    }

    // set loaded fields data to entity properties
    SetPropertiesValues(entity, customFields);
}
The SetPropertiesValues method is defined as following:
internal void SetPropertiesValues(T entity, Dictionary<guid, object> customFieldsValues)
{
    typeof(T).ForeachProperty(propertyInfo =>
    {
        var attr = typeof(T)
                       .GetPropertyCustomAttribute<PSCustomFieldAattribute>(propertyInfo);

        if (attr != null)
        {
            Guid fieldId = attr.FieldUid;
            if (customFieldsValues.ContainsKey(fieldId))
            {
                try
                {
                    object val = customFieldsValues[fieldId];

                    if (propertyInfo.PropertyType.Equals(typeof(int)) && val != null)
                    {
                        val = (int)val;
                    }

                    propertyInfo.SetValue(entity, val, null);
                }
                catch (Exception ex)
                {
                    _logService.Error("Native field mapping exception: " + ex.Message);
                    throw;
                }
            }
        }
    });
}
The GetCustomFieldRowValue method loads proper custom field value from a row using FIELD_TYPE_ENUM column to detect the stored data type.

As you can see the data mapping layer is quite simple and clear to understand. Updating data is much the same. Complete source code you can find at http://fluentps.codeplex.com.

3 comments: