-1

I have a Sql DB with 40 tables and I built models representing each table, I am also using Dapper as my ORM...

I am building my CRUD statements within a DAL (data access layer) that will take myModelObj (coming in via HTTPPost) and modelName (coming in via URL param)

I also have a method (MyMapper) that will map my myModelObj to my models - however the method I created is of type object, which returns System.Object types - what I would like to have happen is the method return the model and convert the data type of the calling method, dynamically/at runtime, to match the model type being returned...

My code:

Controller

private readonly IDbOperation _dbo;
...
...

public DataController(IDbOperation dboperation)
{
      _dbo = dboperation;
}

...

[HttpPost("create/{modelName}", Name = "Generic CRUD: Create Method")]
[Produces("application/json")]
public ActionResult Create([FromBody] JObject createObject, string modelName)
{
  try
  {
      ... ... ...

      object response = _dbo.CreateRecord(createObject, modelName);

// HTTP Post Body example
{
    "Fullname": "Juan Carlos",
    "Firstname": "Juan",
    "Lastname": "Carlos",
    "Salutation": "Dr.",
    "Age": 30,
    "CreatedOn": "2019-11-07T12:25:10"
}

DbOperation

private readonly IDbConnector _dbConnector
...

public DbOperation(IDbConnector dbConnector)
{
      _dbConnector = dbConnector;
}

public JsonResult CreateRecord(JObject model, string modelName)
{
      ... ... ...
      var create = MyMapper(modelName, model); // <<<
      ... ...
      try
      {
            using (var connection = _dbConnector.GetMsSqlConnection())
            {
                 // https://dapper-tutorial.net/insert
                 var createdId = connection.Insert(create); 
                 ...
            }
      }
}

private object MyMapper(string modelName, JObject mod)
{
      switch (modelName.ToLower())
      {
           case "transaction":
                return JsonConvert.DeserializeObject<ModelRepoDTO.Transaction>(model.ToString());

           case "items":
                return JsonConvert.DeserializeObject<ModelRepoDTO.Items>(model.ToString());

           case "users":
                return JsonConvert.DeserializeObject<ModelRepoDTO.Users>(mod.ToString());
      ...
      ...
           default:
                _logger.Error("DataAccessLayer", "DbOperation", ">>> Mapping decision could not be made");
                break;
    }
}

DbConnector

public class DbConnector : IDbConnector
{
    private readonly string _connectionstring;

    public DbConnector(IConfiguration config)
    {
        _connectionstring = config.GetValue<string>("Connectionstring");
    }

    public SqlConnection GetMsSqlConnection()
    {
        SqlConnection conn = null;
        try
        {
            conn = new SqlConnection(_connectionstring);
            conn.Open();
            if (conn.State != ConnectionState.Open)
            {
                // throw error
            }
        }
        catch (Exception ex)
        {
            if (conn != null && conn.State == ConnectionState.Open)
            {
                conn.Close();
            }
            conn = null;
        }

        // return the connection
        return conn;
    }
}

(Currently) The data type coming back from MyMapper("User", {<Object from HTTP Post>}) is System.Object because MyMapper is of type object

What I want is to have the data type dynamically changed to match the model...

MyMapper("User", {<Object from HTTP Post>}) => data type = User

Keep in mind there are 40 different tables/models I can be CRUD'ing against, obviously I won't know which one is being called until runtime...

(to complete the example) ...I take the object returned from MyMapper and pass it to connection.insert(...); (I modified my post to show this - look at DbOperation)

Any suggestions? Thoughts?

Didier Jean Charles
  • 462
  • 2
  • 9
  • 22
  • 4
    A generic method for `MyMapper` would probably be the most elegeant solution... at some point you're going to need to explicitly specify the type you'r eexpecting though, so you won't for example be able to input a JSON string and magically get the expected type without some sort of mapping (probably implemented by a huge `switch` statement) – gabriel.hayes Nov 08 '19 at 21:30
  • 1
    Funny you mention that...the `MyMapper` is a huge `switch` statement...I'll update my post with the some of that just to illustrate – Didier Jean Charles Nov 08 '19 at 21:33
  • `What I want is to have the data type dynamically changed to match the model...` The short answer is you can't do that. You have to think about it as compile time vs runtime. The compiler can only work with what it knows at compile time. At compile time, you told it it was `object` - that is all it knows (unless you change `MyMapper` to return something other than `object` - which you _might_ be able to achieve using generics). – mjwills Nov 08 '19 at 21:36
  • In this example, what do you plan to do with the object returned by `_dbo.CreateRecord()` that requires that it be strongly typed? – John Wu Nov 08 '19 at 21:36
  • Yeah, do you really want to use a giant state machine? A method can only return one unifying type, so you're going to have to cast whatever `MyMapper` returns if it's not generic, unless the downcasted version is all you need (though, right now, it's returning an `object`, which is pretty useless other than for casting). The way you have it setup right now, you'll have to cast twice. once in `DeserializeObject` and then again outside of the function calling `MyMapper`. – gabriel.hayes Nov 08 '19 at 21:36
  • 2
    Design wise, it feels like you are trying to have a single method handle lots of different types but as `object`. In summary - this is going to be painful long term. Either change the mapper to return generics or (I would suggest) making 40 different action methods (endpoints). Sure, there will be some code duplication, but it will be easy to read and reason about. – mjwills Nov 08 '19 at 21:38
  • @mjwills This is the solution we employ at my workplace. Different types go to different endpoints. If the endpoint is `update-user`, we know we need to run `ObjectFromRequestBody()` to get the updated values from the request payload. – gabriel.hayes Nov 08 '19 at 21:39
  • 1
    @user1538301 The OP could even keep their existing URL structure if they wanted. Route `create/bob` to one action, `create/cathy` to another etc etc. – mjwills Nov 08 '19 at 21:40
  • Not to mention if you expose some general purpose endpoint to manipulate all entities used in the .NET code, you'd better have some very strong validation/business policy enforcer. If not you're potentially exposing a bunch of very critical data to users who are able to figure out the data structure of objects that you _haven't_ explicitly exposed to the front-end. I know for us there are some pretty important data objects that users would easily be able to jack around via HTTP calls if what you were asking were possible, and we did it. – gabriel.hayes Nov 08 '19 at 21:42
  • 2
    Change `MyMapper` to use generics - so it will take a `T` type rather than a `string` modelName. When you try and do that, you'll then realise the benefit of the '40 different endpoints' approach (since you need to be able to pass in the generic type at compile time). Yes, this will be some work. :) – mjwills Nov 08 '19 at 21:48
  • Why? Trying to save keystrokes, or is there some run-time savings benefit trying to be achieved? – ΩmegaMan Nov 08 '19 at 22:12
  • What do you do with the result of MyMapper? Does that need to be strongly typed? If so I don't get how you're using it, looks like you call it one time and no code showing how you use the result. If you're truly making it dynamic (not judging), maybe try the `dynamic` keyword? Generics is probably a better path though and purists won't come after you with pitchforks... – MrRobboto Nov 08 '19 at 22:21
  • I updated the example to show what I am doing with `MyMapper` – Didier Jean Charles Nov 09 '19 at 01:19
  • In your updated example, what type is `connection`? – John Wu Nov 09 '19 at 02:14
  • @JohnWu - `var connection = new SqlConnection(myConnectionString)` - nothing fancy – Didier Jean Charles Nov 09 '19 at 14:01
  • SqlConnection does not have an `insert` method. – John Wu Nov 09 '19 at 15:56
  • @JohnWu - you are right let me correct myself - I have a DBConnector class that contains a method `GetMsSqlConnection()` of type `SqlConnection` which opens & returns a connection - again nothing fancy – Didier Jean Charles Nov 09 '19 at 16:13
  • OP, I think maybe I'm not getting my point across. In order to understand what you are doing with the result of MyMapper I need to understand the method you are passing it to-- most important, I need to know the method definition, e.g. its parameter type, whether it is generic, and so on. These are not unimportant details. Please edit your question to include. – John Wu Nov 09 '19 at 17:38
  • This question is a classic example of the XY Problem - https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem . In future, the OP should consider sharing all of the context **up front**. – mjwills Nov 09 '19 at 21:41
  • Yeah, need to know the signature of Insert() with `connection.Insert(what is it expecting?)` - looks like `dynamic` may help you here but not enough context still... – MrRobboto Nov 09 '19 at 21:57
  • @JohnWu / @MrRobboto - I definitely understand and appreciate what you are saying about `connection.Insert` -- I added my `DbConnector` class to my post, but keep in mind, `.Insert(...)` is a method from the Dapper ORM/framework - and it wants to take the object that you want to insert... One of the SO posts that I was reading https://stackoverflow.com/questions/5957774/performing-inserts-and-updates-with-dapper - highlights a similar approach, however, I won't know what Class to insert until runtime - – Didier Jean Charles Nov 10 '19 at 12:58

1 Answers1

3

If I understand your question, you are attempting to call a third party (Dapper) generic method without foreknowledge of the generic type argument, which normally must be supplied at compile time.

This can be accomplished via Reflection. In your wrapper class (which I believe you call "DbConnector") add a new method called "InsertDynamicObject" and code it to use Reflection to find the method definition that you wish to call. You can then call MakeGenericMethod, passing the generic type parameters. An example could look like this:

namespace Dapper  //This is a stub for the Dapper framework that you cannot change
{
    class DbConnection
    {
        public void Insert<T>(T obj)
        {
            Console.WriteLine("Inserting an object with type {0}", typeof(T).FullName);
        }
    }
}

namespace MyProgram
{
    class DbConnector  //Here is your DbConnector class, which wraps Dapper
    {
        protected readonly Dapper.DbConnection _connection = new Dapper.DbConnection();

        public void InsertDynamicObject(object obj)
        {
            typeof(Dapper.DbConnection)
                .GetMethod("Insert")
                .MakeGenericMethod(new [] { obj.GetType() })
                .Invoke(_connection, new[] { obj });
        }
    }

    public class Program
    {
        public static void Main()
        {
            object someObject = "Test";  //This is the object that you deserialized where you don't know the type at compile time.

            var connector = new DbConnector();
            connector.InsertDynamicObject(someObject);
        }
    }
}

Output:

Inserting an object with type System.String

Here is a link to a working example on DotNetFiddle: Link

John Wu
  • 50,556
  • 8
  • 44
  • 80