2

I have some C# classes which represent database objects, some of which contain one or more other custom objects or enumerables of custom objects. I'm using dapper for queries, and slapper to map to the custom objects. It works great for single object. I can easily grab a parent object with a specific ID from the database, do some inner joins, and map it and all the things it "owns" to my custom objects in C#. Problem comes when I want to do a select over multiple parent-IDs.

Some context, let's say I have a person, that person has a list of hobbies which have an ID and a Description, a list of days they're available which also have an ID and Description, and maybe another custom field such as whether they have or are even willing to be around children which can also boil down to a simple ID and Description. We'll call that last field child status. I'd write a select statement like this:

SELECT 
  ,person.id as Id
  ,person.first_name as FirstName
  ,person.last_name as LastName
  ,hobby.Id as Hobbies_Id
  ,hobby.Description as Hobbies_Description
  ,avail.Id as Availabilities_Id
  ,avail.Description as Availabities_Description
  ,child.Id as ChildStatus_Id
  ,child.Description as ChildStatus_Description
FROM
  users.users person
JOIN
  users.userhobbies uhobby
ON
  person.id = uhobby.UserId -- one-to-many with relational table
JOIN
  users.avail hobby
ON
  uhobby.HobbyId = hobby.Id
JOIN
  users.useravailabilities uavail
ON
  person.id = uavail.UserId -- one-to-many with relational table
JOIN
  users.availabilities avail
ON
  uavail.AvailId = avail.Id
JOIN
  users.childstatuses child
ON
  person.ChildStatusId = child.Id

Then I want this mapped to a user like this:

class User 
{
  public Guid Id {get; set;}
  public string FirstName {get; set;}
  public string LastName {get; set;}
  public IEnumerable<Hobby> Hobbies {get; set;}
  public IEnumerable<Availability> Availabilities {get; set;}
  public ChildStatus ChildStatus {get; set;}
}

Since I'm using exact naming conventions and everything here, the query from Dapper and the Automapping work great just like this:

  // Using the above sql in a variable
  var data = Connection.Query<dynamic>(sql);

  var dataReal = Slapper.AutoMapper.MapDynamic<User>(data);
  return dataReal;

This works great, but it only ever returns one user. I have a similar method which takes an ID and all of my test users can be retrieved perfectly by passing the ID. I've tried scouring the internet, looking through documentation, and all I found was this: https://github.com/SlapperAutoMapper/Slapper.AutoMapper/issues/57 who seemed to just slip through the cracks. I also tried mapping the dynamic data to various other structures with no luck. Thanks in advance!

Update: I've come up with a somewhat brutal, "sledgehammer" type solution. I'm not sure if, at this point, I'm forcing myself to use Slapper when there might be a more convenient solution. However, I wanted to ensure anyone in a similar situation might have a chance at making it work. Here's the new C# section:

  var data = Connection.Query<dynamic>(sql);
  IEnumerable<Guid> Ids = data.Select(row => (Guid)row.id).Distinct();
  List<User> results = new List<User>();

  foreach (Guid Id in Ids)
  {
    IEnumerable<dynamic> rows = data.Where(x => { return ((Guid) x.id).Equals(Id); });
    User model = (Slapper.AutoMapper.MapDynamic<User>(rows, false) as IEnumerable<User>).FirstOrDefault();
    if (model != null)
    {
      results.Add(model); 
    }
  }

  return results;

As you can see, I'm generating a list of unique "primary object" ID's and selecting those rows into their own lists, which I then pass to Slapper. I've passed the "cache = false" parameter to avoid squeezing unrelated data into every object after the first. I could probably get around this by actually keeping the UserHobby/UserAvailability/UserPhoto Ids in place, but I don't like the way that makes my object look. Hopefully this helps someone.

Joe.Flanigan
  • 736
  • 6
  • 18

1 Answers1

0

I'm not familiar with Slapper, but I'll show you what I've done with Dapper to construct a complex graph of objects with bi-directional references.

In short, construct a Dictionary or KeyedCollection prior to calling connection.Query<>, then reference it inside the Dapper lambda expression.

This method returns a list of service calls. Each service call is assigned to one technician and one customer. However, a technician may be assigned multiple service calls to multiple customers. And a customer may have multiple technicians on-site.

public ServiceCallResponse GetServiceCallsDapper(ServiceCallRequest Request)
{
    var queryParameters = new {statuses = Request.Statuses, createDate = Request.CreateDate};
    const string splitOn = "Number,Id"; // Id indicates beginning of second class (Technician).  Number indicates begining of third class (Customer).
    // Note multiple columns are named "Number".  See note below about how Dapper maps columns to class properties.
    // Note Dapper supports parameterized queries to protect against SQL injection attacks, including parameterized "where in" clauses.
    const string query = @"sql query here..."
    ServiceCallResponse response = new ServiceCallResponse();  // Keyed collection properties created in constructor.
    using (IDbConnection connection = new SqlConnection("DB connection string here..."))
    {
        connection.Open();

        // Dapper adds a generic method, Query<>, to the IDbConnection interface.

        // Query<(1)ServiceCall, (2)Technician, (3)Customer, (4)ServiceCall> means
        //   construct a (1)ServiceCall, (2)Technician, and (3)Customer class per row, add to an IEnumerable<(4)ServiceCall> collection, and return the collection.

        // Query<TFirst, TSecond, TThird, TReturn> expects SQL columns to appear in the same order as the generic types.
        // It maps columns to the first class, once it finds a column named "Id" it maps to the second class, etc.
        // To split on a column other than "Id", specify a splitOn parameter.
        // To split for more than two classes, specify a comma-delimited splitOn parameter.

        response.ServiceCalls.AddRange(connection.Query<ServiceCall, Technician, Customer, ServiceCall>(query, (ServiceCall, Technician, Customer) =>
        {
            // Notice Dapper creates many objects that will be discarded immediately (Technician & Customer parameters to lambda expression).
            // The lambda expression sets references to existing objects, so the Dapper-constructed objects will be garbage-collected.
            // So this is the cost of using Dapper.  We trade unnecessary object construction for simpler code (compared to constructing objects from IDataReader).

            // Each row in query results represents a single service call.
            // However, rows repeat technician and customer data through joined tables.
            // Avoid constructing duplicate technician and customer classes.

            // Refer to existing objects in global collections, or add Dapper-mapped objects to global collections.
            // This avoid creating duplicate objects to represent same data.
            // Newtonsoft JSON serializer preserves object instances from service to client.
            Technician technician;
            Customer customer;
            if (response.Technicians.Contains(Technician.Id))
            {
                technician = response.Technicians[Technician.Id];
            }
            else
            {
                response.Technicians.Add(Technician);
                technician = Technician;
            }
            if (response.Customers.Contains(Customer.Number))
            {
                customer = response.Customers[Customer.Number];
            }
            else
            {
                response.Customers.Add(Customer);
                customer = Customer;
            }
            // Set object associations.
            ServiceCall.Technician = technician;
            ServiceCall.Customer = customer;
            technician.ServiceCalls.Add(ServiceCall);
            if (!technician.Customers.Contains(customer))
            {
                technician.Customers.Add(customer);
            }
            customer.ServiceCalls.Add(ServiceCall);
            if (!customer.Technicians.Contains(technician))
            {
                customer.Technicians.Add(technician);
            }
            return ServiceCall;
        }, queryParameters, splitOn: splitOn));
    }
    return response;
}

Using this technique requires you to set PreserveReferencesHandling = true on the JsonSerializer class so object references are preserved on the client-side. Otherwise, Json.NET will construct duplicate objects and technician.Customers.Count will always == 1.

For example, if John Doe is assigned a service call at Acme and another at Contoso, his technician.Customers.Count will equal 1 if you leave PreserveReferencesHandling == false (Json.NET will construct two Technician objects each named John Doe).

Erik Madsen
  • 497
  • 6
  • 17