简体   繁体   中英

Instantiate a concrete class based on generic parameter c#

Well, I tried to wrap my mind around this with no luck; can't find a better solution so I would gladly use some help/ideas.

Basically, I have a generic method which receives a businessobject as generic parameter and it should instantiate a EntityFramework entity and return it. There is a naming convention between the businessobject name and the entity framework name (ie if the BO is named 'UsersBo', then the EF entity is named 'Users') and that convention is enforced everywhere.

My code looks like this:

public dynamic CreateEntity<T>()
{
    switch (typeof(T).Name)
    {
        case "UsersBo":
           return new Users();
        case "RolesBo":
           return new Roles();
        case "AppObjectsBo":
           return new AppObjects();
        default:
           break;
    }
    return null;
}

I have two questions:

  1. Given the fact the BO and EF entity are located in different DLLs (BLL.dll vs DAL.dll), how the heck do I instantiate it without that ugly switch command? Tried with Activator.CreateInstance and it gave me errors regarding "fully qualified name"

  2. Is there any way to NOT use Dynamic objects? Right now I'm using dynamic becase I can't know which concrete class will be instantiated.

Thank you

================================ Later edit:

Here's the actual code:

public T GetById(Guid id)
        {
            T entityBo = new T();
            var entity = new EntityFactory().CreateEntity<T>();
            var repository = new EntityFactory().CreateRepository<T>();
            try
            {
                entity = repository.GetById(id);
                if (entity != null)
                {
                    Mapper.Map(entity, entityBo);
                }
            }
            catch (Exception ex)
            {
                entityBo = default(T);
                throw ex.GetBaseException();
            }
            return entityBo;
        }

===================== I have a generic service class which works with business objects. Of course, these must be mapped to the corresponding EF objects.

If I'm calling the above method using UsersBo as T, the code will become:

public UsersBo GetById(Guid id)
        {
            UsersBo entityBo = new UsersBo(); // no problem here
            var entity = new EntityFactory().CreateEntity<T>(); // That's the problem line: T is UsersBo, but I need a new Users (EF entity) in order to do the mapping later
            var repository = new EntityFactory().CreateRepository<T>();
            try
            {
                entity = repository.GetById(id); // this one returns the EF entity
                if (entity != null)
                {
                    Mapper.Map(entity, entityBo);
                }
            }
            catch (Exception ex)
            {
                entityBo = default(T);
                throw ex.GetBaseException();
            }
            return entityBo;
        }

Hopefully now makes more sense. I am open to any suggestions, maybe I'm completely off and there's a better way.

Thank you guys.

=============

Slept over it. Top-notch answers, already changed the code and works like a charm. Thank you, guys, you were extremely helpful.

Activator.CreateInstance is correct, but you need to specify the fully qualified name of the class, in your case something like this:

string className = T.GetType().Name.Replace("Bo", string.Empty);
var fullClassName = string.Concat("Namespace.", className, ",BLL.dll");
object obj = Activator.CreateInstance("BLL.dll", fullClassName);
return obj;

The fully qualified name of a class contains the full path of namespaces. In the CreateInstance method you need to specify also the DLL name where the class is defined. Please replace "Namespace." with the actual namespace.

You could do something like this but I still do not like the overall design because you are forgoing type safety with the keyword dynamic . It would be better to use the passed in type constraint to get back the exact type you want to use in the calling code.

// personally i would change dynamic to object
// calling code could always cast to dynamic if it needed to
public dynamic CreateEntity<T>()
{
    var bllName = typeof(T).Name;
    var efName = bllName.Substring(0, bllName.Length - 2); // removes "Bo"

    var className = string.Concat("Namespace.", efName, ", EfAssembly.dll");
    var efType = Type.GetType(className);
    return efType != null ? Activator.CreateInstance(efType) : null;
}

This (see code below) would be my preference, here T would be the actual EF entity that you want to work with in the calling code and is not related at all to your BLL type. Alternatively you could enrich your BLL types to return the correct EF types but the negative on that is SoC as you would be breaking the loose coupling rule.

public T CreateEntity<T>() where T : class, new()
{
    return new T();
}

Based on your latest edit I refined your code with how you could approach this.

public T GetById<T>(Guid id) where T : new()
{
    T entityBo = new T();

    // this should not be needed, you do not use the results anywhere unless your EF type provides for some defaults at the BLL layer but it would be better to include those defaults in the BLL types constructor instead
    // var entity = new EntityFactory().CreateEntity<T>(); // That's the problem line: T is UsersBo, but I need a new Users (EF entity) in order to do the mapping later

    // this is the line where you need to implement the translation to get the correct repository type
    var repository = new EntityFactory().CreateRepository<T>();
    try
    {
        // get your object from your repository or if it returns null then you will end up just returning the default bll instance which is OK
        var entity = repository.GetById(id);
        if (entity != null)
        {
            // map what was returned
            Mapper.Map(entity, entityBo);
        }
    }
    catch (Exception ex)
    {
        // no need for this, it adds nothing
        // entityBo = default(T);

        // do not do this
        // throw ex.GetBaseException();

        // this is better, it preserves the stack trace and all exception details
        throw;
    }
    return entityBo;
}

You could creating a mapper between your business object and your entity objects.

[TestClass]
public class EFMappingTest {
    [TestMethod]
    public void MappperTest() {
        EntityFactory.Map<UsersBo, Users>();
        EntityFactory.Map<RolesBo, Roles>();
        EntityFactory.Map<AppObjectsBo, AppObjects>();

        var factory = new EntityFactory();

        var entity = factory.CreateEntity<UsersBo>();

        Assert.IsNotNull(entity);
        Assert.IsInstanceOfType(entity, typeof(Users));
    }

    class Users { }
    class UsersBo { }
    class Roles { }
    class RolesBo { }
    class AppObjects { }
    class AppObjectsBo { }

    public class EntityFactory {
        static IDictionary<Type, Type> mappings = new Dictionary<Type, Type>();

        public static void Map<TBusinessObject, TEntity>() where TEntity : class, new() {
            Map(typeof(TBusinessObject), typeof(TEntity));
        }

        public static void Map(Type sourceType, Type targetType) {
            mappings[sourceType] = targetType;
        }

        public object CreateEntity<T>() {
            Type entityType = null;
            return mappings.TryGetValue(typeof(T), out entityType)
                ? Activator.CreateInstance(entityType)
                : null;
        }

        public TEntity CreateEntity<TEntity>(object businessObject) where TEntity : class {
            if (businessObject == null) throw new ArgumentNullException("businessObject");
            Type businessObjectType = businessObject.GetType();
            Type entityType = null;
            return mappings.TryGetValue(businessObjectType, out entityType)
                ? (TEntity)Activator.CreateInstance(entityType)
                : null;
        }
    }
}

Given your concern about the number of mapping to make. You said you have a convention. You can automate the mappings by enumerating your business objects and searching for entities that match your convention and then map them.

EDIT: one other solution added

I have 3 solutions to propose, but it works only if you have interfaces for both your entities and your model objetcs:

Something like @Igor Damiani said:

TInterface Instantiate<TEntity, TInterface>()
        where TEntity : class, TInterface
        where TInterface : class
{
    return (TInterface)Activator.CreateInstance(Type.GetType("Namespace" + typeof(TEntity).FullName.Replace("Bo", "")));
}

Usage: IEntity instance = Instantiate<EntityType, IEntity>();

The dictionary method:

Dictionary<Type, Type> _types = new Dictionary<Type, Type>
{
    { typeof(IEntityA), typeof(ModelA) },
    { typeof(IEntityB), typeof(ModelB) }
};

TInterface Instantiate<TInterface>()
        where TInterface : class
{
    return (TInterface)Activator.CreateInstance(Type.GetType(_types[typeof(TInterface)].FullName.Replace("1", "2")));
}

Usage: IEntity instance = Instantiate<IEntity>();

The third

would be an hybrid between the @Igor Damiani, @Nkosi's solutions to fill the dictionary and call the Activator.CreateInstance method.

But all of this, works only if you have interface.


Edit: The T4 solutions

Entity Framework Edmx is a T4 template. Then you can edit it to generate your old class (if you use the assembly of your models in the same assembly as the edmx).

Or you can generate the dictionary with the edmx.

(Whatever, creating the links between your entities and your models, can become really easy thanks to the edmx)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM