vendredi 23 août 2019

C# Interfaces & Generics working together

I have a domain object, Address that may be populated from a variety of data sources, which requires a lot of mapping code. In the interest of "Closed to Modification" I want to be able to create individual "Mappers" for each data source. I can then pass the mapper into an instance of the Address and VOILA! get an appropriate data entity back in response. And vice-versa, I also want implement a method on that Address that will allow me to map an entity into a new or to populate an existing instance of the Address.

I create my Address object ...

public class Address
{
    public string Street1 { get; set; }
    public string Street2 { get; set; }
    public string Street3 { get; set; }
    public string Street4 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Country { get; set; }
    public string PostalCode { get; set; }
}

Now I create a couple of classes that will facilitate mapping specific data entity objects to and from this Address object.

//
// Maps to and from a Database object (DB1_ADDRESS)
//
public class DB1AddressMapper
{
    property DB1_ADDRESS _entity;

    public DB1AddressMapper()
    {

    } 

    public DB1AddressMapper(DB1_ADDRESS entity)
    {
        _entity = entity;
    }

    public DB1_ADDRESS MapModelToEntity(Address model)
    {
        DB1_ADDRESS ret = new DB1_ADDRESS();

        <... mapping logic goes here>

        return ret;
    }

    public Address MapEntityToModel()
    {
        Address ret = new Address();

        <... mapping logic goes here>

        return ret;
    }
}

//
// Maps to and from a WebService response (WS_ADDRESS)
//
public class WSAddressMapper
{
    property WS_ADDRESS _entity;

    public WSAddressMapper()
    {

    } 

    public WSAddressMapper(WS_ADDRESS entity)
    {
        _entity = entity;
    }

    public WS_ADDRESS MapModelToEntity(Address model)
    {
        WS_ADDRESS ret = new WS_ADDRESS();

        <... mapping logic goes here>

        return ret;
    }

    public Address MapEntityToModel()
    {
        Address ret = new Address();

        <... mapping logic goes here>

        return ret;
    }
}

Now that I have my mappers I create a method on Address that I can pass them into, in order to facilitate transforming the data. So you can see in the code below that I have had to overload the methods because each mapper has its own types involved. This means that every time I want to add a new data source to populate the Address object I have to re-open Address and add new overload methods. Ugghhh ... no thanks you (what happened to "closed for modification"?)

public class Address
{
    public string Street1 { get; set; }
    public string Street2 { get; set; }
    public string Street3 { get; set; }
    public string Street4 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Country { get; set; }
    public string PostalCode { get; set; }
    //
    // Populate "this" instance of the Address object from data found in the mapper.
    // The "mapper" argument would have to have been instantiated with the entity it expects to map 
    // to the Domain object, Address
    //
    public Address MapToModel(DB1AddressMapper mapper)
    {
        return mapper.MapEntityToModel();
    }

    //
    // Map "this" instance of address to a new DB1_ADDRESS instance
    //
    public DB1_ADDRESS MapToEntity(DB1AddressMapper mapper)
    {
        return mapper.MapModelToEntity(this);
    }


    //
    // And now again for WSAddressMapper
    //
    public Address MapToModel(WSAddressMapper mapper)
    {
        return mapper.MapEntityToModel();
    }

    //
    // Map "this" instance of address to a new WS_ADDRESS instance
    //
    public WS_ADDRESS MapToEntity(WSAddressMapper mapper)
    {
        return mapper.MapModelToEntity(this);
    }

} 

This leads me to interfaces and generics ... which I have dabbled in for years but the lack of necessity for them has not forced me to deepen my understanding of them (which I believe holds me back).

Back to the problem at hand ... I only want two mapping methods in Address that will be "closed for modification". They need to accommodate any mapper for any data source I run into. The mapper encapsulates all the specific mapping logic and Address doesn't really care about the details. It just wants to "MapTo".

The pseudo-code solution looks something like this ...

public class Address 
{
    public Address MapToModel(EntityMapper mapper)
    {
        ...
    }

    public EntityAddress MapToEntity(EntityMapper mapper)
    {
        ...
    }
}

It seems that I could make an interface for the mappers so that all mappers will implement the same two methods ...

MapModelToEntity();
MapEntityToModel();

I start with that ...

public interface IEntityAddressMapper
{
    Address MapEntityToModel();
    T MapModelToEntity<T>(Address model);
}

You may be able to see where I start to run into trouble. Since the return type of the "MapModelToEntity" varies from data source to data source I don't know what to make this. I opt to make it a generic; those have worked for me in other areas. I press on by implementing it in my mappers in hopes that an answer will reveal itself.

public class DB1AddressMapper : IEntityAddressMapper
{
    Address MapEntityToModel()
    {
        Address ret = new Address();

        <... mapping logic goes here>

        return ret;
    }

    //
    // This is what I want but, does NOT satisfy interface
    //
    DB1_ADDRESS MapModelToEntity(Address model)  <!-- DOES NOT SATISFY INTERFACE
    {
        DB1_ADDRESS ret = new DB1_ADDRESS();

        <... mapping logic goes here>

        return ret;
    }
    //
    // This satisfies interface but is silly. The mapper already KNOWS the TYPE, that's the point.
    // Besides this means that the consumer will have to pass in the types, which is EXACTLY what 
    // I am trying to avoid.
    //
    T MapModelToEntity<T>(Address model)  
    {
        DB1_ADDRESS ret = new DB1_ADDRESS();

        <... mapping logic goes here>

        return ret;
    }   
}

I've tried a million different permutations so it's impractical to list them all here but suffice to say the closest I have come so far is the following ...



public interface IEntityAddressMapper<EntityType>
{

    EntityType MapModelToEntity(Address mode);

    void MapModelToEntity(Address model, ref EntityType entity);

    Address MapEntityToModel(EntityType entity);

    void MapEntityToModel(EntityType entity, ref Address model);
}



public class DB1AddressMapper : IEntityAddressMapper<DB1_ADDRESS>
{
    Address MapEntityToModel()
    {
        Address ret = new Address();

        <... mapping logic goes here>

        return ret;
    }

    T MapModelToEntity(Address model)  
    {
        DB1_ADDRESS ret = new DB1_ADDRESS();

        <... mapping logic goes here>

        return ret;
    }   
}

This seems to allow me to implement the Interface without a problem now, but I seem to have shifted the burden to the methods which now are breaking ...

public class Address 
{
    // *********************************************
    // ERROR - Using generic type 'IEntityAddressMapper<EtityType>' requires one type argument
    // *********************************************
    public Address MapToModel(EntityMapper mapper)
    {
        ...
    }

    // *********************************************
    // ERROR - Using generic type 'IEntityAddressMapper<EtityType>' requires one type argument
    // *********************************************
    public EntityAddress MapToEntity<EntityType>(EntityMapper mapper)
    {
        ...
    }
}

I'm spinning in circles and have been for years on this. I need to sort this out!! Any help would be greatly appreciated.

Thanks

Aucun commentaire:

Enregistrer un commentaire