views:

211

answers:

3

I have a data access layer which returns an IDataRecord. I have a WCF service that serves DataContracts (dto's). These DataContracts are initiated by a parametrized constructor containing the IDataRecord as follows:

[DataContract]
public class DataContractItem
{
    [DataMember]
    public int ID;
    [DataMember]
    public string Title;

    public DataContractItem(IDataRecord record)
    {
        this.ID = Convert.ToInt32(record["ID"]);
        this.Title = record["title"].ToString();
    }
}

Unfortanately I can't change the DAL, so I'm obliged to work with the IDataRecord as input. But in generat this works very well. The mappings are pretty simple most of the time, sometimes they are a bit more complex, but no rocket science.

However, now I'd like to be able to use generics to instantiate the different DataContracts to simplify the WCF service methods. I want to be able to do something like:

public T DoSomething<T>(IDataRecord record) {
    ...
return new T(record);
}

So I'd tried to following solutions:

  1. Use a generic typed interface with a constructor. doesn't work: ofcourse we can't define a constructor in an interface

  2. Use a static method to instantiate the DataContract and create a typed interface containing this static method. doesn't work: ofcourse we can't define a static method in an interface

  3. Use a generic typed interface containing the new() constraint doesn't work: new() constraint cannot contain a parameter (the IDataRecord)

  4. Using a factory object to perform the mapping based on the DataContract Type. does work, but: not very clean, because I now have a switch statement with all mappings in one file.

I can't find a real clean solution for this. Can somebody shed a light on this for me? The project is too small for any complex mapping techniques and too large for a "switch-based" factory implementation.

+2  A: 

You can invoke a types constructor through reflection:

public T DoSomething<T>(IDataRecord record)
{
    //do something...

    var ci = typeof(T).GetConstructor(new[] { typeof(IDataRecord) });
    return (T)ci.Invoke(new object[] { record });
}

EDIT: The above approach is quite brittle and relies on convention, so another approach would be to create an interface for your data contracts which allow initialisation:

public interface IDataContract
{
    void Initialise(IDataRecord record);
}

public T DoSomething<T>(IDataRecord record) where T : IDataContract, new()
{
    //do something

    T contract = new T();
    contract.Initialise(record);

    return contract;
}
Lee
true, but I want to be able to be sure that all DataContracts implement that constructor, hence I'd like to use an interface for the DataContracts. However if I use reflection I am not sure that the DataContract being "constructed" has the correct constructor.
Rody van Sambeek
+1  A: 

Here's how I do it..

Extension methods for DataRow that lets me transform a Business Object to DataRow and vice-versa.

/// <summary>
/// Extension methods for DataRow.
/// </summary>
public static class DataRowExtensions
{
    /// <summary>
    /// Converts DataRow into business object.
    /// </summary>
    /// <typeparam name="TEntity">A type that inherits EntityBase.</typeparam>
    /// <param name="dataRow">DataRow object to convert to business object.</param>
    /// <returns>business object created from DataRow.</returns>
    public static TEntity ToEntity<TEntity>(this DataRow dataRow)
        where TEntity : EntityBase, new()
    {
        TEntity entity = new TEntity();
        ExtensionHelper.TransformDataRowToEntity<TEntity, DataRow>(ref dataRow, ref entity);

        return entity;
    }

    /// <summary>
    /// Converts business object into DataRow.
    /// </summary>
    /// <typeparam name="TEntity">A type that inherits EntityBase.</typeparam>
    /// <param name="dataRow">DataRow object to convert business object into.</param>
    /// <param name="entity">Business object which needs to be converted to DataRow.</param>
    public static void FromEntity<TEntity>(this DataRow dataRow, TEntity entity)
        where TEntity : EntityBase, new()
    {
        ExtensionHelper.TransformEntityToDataRow<TEntity, DataRow>(ref entity, ref dataRow);
    }
}

The actual helper methods that does the transform..

/// <summary>
/// Helper methods for transforming data objects into business objects and vice versa.
/// </summary>
/// <remarks>
/// <para>Most important implementation that takes care of universal transformation between business objects and data object.</para>
/// <para>Saves programmers from writing the same old code for every object in the system.</para>
/// </remarks>
public static class ExtensionHelper
{
    /// <summary>
    /// Transforms business object into DataRow.
    /// </summary>
    /// <typeparam name="TEntity">A type that inherits EntityBase.</typeparam>
    /// <typeparam name="TDataRow">A type that inherits DataRow.</typeparam>
    /// <param name="entity">business object which is transformed into DataRow object.</param>
    /// <param name="dataRow">DataRow object which is transformed from business object.</param>
    public static void TransformEntityToDataRow<TEntity, TDataRow>(ref TEntity entity, ref TDataRow dataRow)
        where TDataRow : DataRow
        where TEntity : EntityBase
    {
        IQueryable<DataField> entityFields = entity.GetDataFields();

        foreach (DataColumn dataColoumn in dataRow.Table.Columns)
        {
            if (!dataColoumn.ReadOnly)
            {
                var entityField =
                    entityFields.Single(e => e.DataFieldMapping.MappedField.Equals(dataColoumn.ColumnName, StringComparison.OrdinalIgnoreCase));

                if (entityField.Property.GetValue(entity, null) == null)
                {
                    if (dataColoumn.AllowDBNull)
                    {
                        dataRow[dataColoumn] = System.DBNull.Value;
                    }
                    else
                    {
                        throw new Exception(dataColoumn.ColumnName + " cannot have null value.");
                    }
                }
                else
                {
                    if (entityField.Property.GetType().IsEnum)
                    {
                        dataRow[dataColoumn] = Convert.ToByte(entityField.Property.GetValue(entity, null));
                    }
                    else
                    {
                        dataRow[dataColoumn] = entityField.Property.GetValue(entity, null);
                    }
                }
            }
        }
    }

    /// <summary>
    /// Transforms DataRow into business object.
    /// </summary>
    /// <typeparam name="TEntity">A type that inherits EntityBase.</typeparam>
    /// <typeparam name="TDataRow">A type that inherits DataRow.</typeparam>
    /// <param name="dataRow">DataRow object which is transformed from business object.</param>
    /// <param name="entity">business object which is transformed into DataRow object.</param>
    public static void TransformDataRowToEntity<TEntity, TDataRow>(ref TDataRow dataRow, ref TEntity entity)
        where TDataRow : DataRow
        where TEntity : EntityBase
    {
        IQueryable<DataField> entityFields = entity.GetDataFields();

        foreach (var entityField in entityFields)
        {
            if (dataRow[entityField.DataFieldMapping.MappedField] is System.DBNull)
            {
                entityField.Property.SetValue(entity, null, null);
            }
            else
            {
                if (entityField.Property.GetType().IsEnum)
                {
                    Type enumType = entityField.Property.GetType();
                    EnumConverter enumConverter = new EnumConverter(enumType);
                    object enumValue = enumConverter.ConvertFrom(dataRow[entityField.DataFieldMapping.MappedField]);
                    entityField.Property.SetValue(entity, enumValue, null);
                }
                else
                {
                    entityField.Property.SetValue(entity, dataRow[entityField.DataFieldMapping.MappedField], null);
                }
            }
        }
    }
}

And here's how my sample Business-object looks. Please mind the Custom attributes..

/// <summary>
/// Represents User.
/// </summary>
[TableMapping("Users", "User", "Users")]
public class User : EntityBase
{
    #region Constructor(s)
    /// <summary>
    /// Initializes a new instance of the User class.
    /// </summary>
    public User()
    {
    }
    #endregion

    #region Properties

    #region Default Properties - Direct Field Mapping using DataFieldMappingAttribute

    /// <summary>
    /// Gets or sets the ID value of the AppUser object.
    /// </summary>
    [DataFieldMapping("UserID")]
    [DataObjectFieldAttribute(true, true, false)]
    [NotNullOrEmpty(Message = "UserID From UserDetails Table Is Required.")]
    public override int Id
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or sets the Username value of the User object.
    /// </summary>
    [DataFieldMapping("UserName")]
    [Searchable]
    [NotNullOrEmpty(Message = "Username Is Required.")]
    public string UserName
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or sets the FirstName value of the AppUser object.
    /// </summary>
    [DataFieldMapping("FirstName")]
    [Searchable]
    public string FirstName
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or sets the LastName value of the AppUser object.
    /// </summary>
    [DataFieldMapping("LastName")]
    [Searchable]
    public string LastName
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or sets the WebSite value of the AppUser object.
    /// </summary>
    [DataFieldMapping("WebSite")]
    [ValidURL(Message = "Website is not in Proper Format.")]
    public string WebSite
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or sets the ContactNumber value of the AppUser object.
    /// </summary>
    [DataFieldMapping("ContactNumber")]
    [Searchable]
    public string ContactNumber
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or sets a value indicating whether AppUser Object is active or inactive.
    /// </summary>
    [DataFieldMapping("IsActive")]
    public bool IsActive
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or sets the BirthDate value of the AppUser object.
    /// </summary>
    [DataFieldMapping("BirthDate")]
    public DateTime? BirthDate
    {
        get;
        set;
    }

    #region Derived Properties

    /// <summary>
    /// Gets the full name of the AppUser
    /// </summary>
    public string FullName
    {
        get { return this.FirstName + " " + this.LastName; }
    }

    /// <summary>
    /// Gets the Age value of the AppUser
    /// </summary>
    public int Age
    {
        get { return this.BirthDate.HasValue ? this.BirthDate.Value.AgeInYears() : 0; }
    }

    #endregion

    #endregion

}

And here's my client-code..

    /// <summary>
    /// Gets User object by user name.
    /// </summary>
    /// <param name="username">UserName of the user</param>
    /// <returns>User Object</returns>
    public static User GetUserByUsername(string username)
    {
        try
        {
            return Adapter.GetUserByUserName(username)[0].ToEntity<User>();
        }
        catch
        {
            return null;
        }
    }
this. __curious_geek
+1  A: 

I think you could use AutoMapper for this. It is an open source project and it would allow to do what you want. In your do something you would do this:

public TDestination DoSomething<TDestination>(IDataRecord record) {
   return Mapper.Map(reader, reader.GetType(), typeof(TDestination));
}

Before you can do that you would set up a map

Mapper.CreateMap<IDataRecord, MyDestinationType>()
epitka
I actually went this way. I knew automapper, but I assumed it was too complex for this case. However it turns out is was hardly complex and very easy! Thanks!
Rody van Sambeek