Creating custom objects with custom properties using generics

Posted on

Problem

I have two base classes: City and Building:

public class City
{
    public string Name { get; set; }
    public double Area { get; set; }
}

public class Building
{
    public string Name { get; set; }
    public double Area { get; set; }
    public int Stories { get; set; }
}

With two classes that inherit from Building, with their own Id property:

public class MyBuilding : Building
{
    public int MyId { get; set; }
}

public class HisBuilding : Building
{
    public int HisId { get; set; }
}

And two classes that inherit from City, with their own Id and IEnumerable<Building> properties:

public class MyCity : City
{
    public int MyId { get; set; }
    public IEnumerable<MyBuilding> Buildings { get; set; }
}

public class HisCity : City
{
    public int HisId { get; set; }
    public IEnumerable<HisBuilding> Buildings { get; set; }
}

Below is a method I’ve created that returns a KeyValuePair<City, Building> using generics:

public static KeyValuePair<TCity, IEnumerable<TBuilding>> GetData<TCity, TBuilding>()
    where TCity : City, new()
    where TBuilding : Building, new()
{
    TCity city = new TCity();
    IEnumerable<TBuilding> buildings = new List<TBuilding>();
    return new KeyValuePair<TCity, IEnumerable<TBuilding>>(city, buildings);
}

And in my Main method I’ve logic that instantiates a specific City object based on a user’s input.

int input = -1;
string strInput = Console.ReadLine();
int.TryParse(strInput, out input);
string json = string.Empty;
switch (input)
{
    case 1:
        KeyValuePair<MyCity, IEnumerable<MyBuilding>> myCity
            = GetData<MyCity, MyBuilding>();
        MyCity my = myCity.Key;
        my.MyId = 1;
        my.Buildings = myCity.Value;
        json = JsonConvert.SerializeObject(my, Formatting.Indented);
        break;

    default:
        KeyValuePair<HisCity, IEnumerable<HisBuilding>> hisCity
            = GetData<HisCity, HisBuilding>();
        HisCity his = hisCity.Key;
        his.HisId = 2;
        his.Buildings = hisCity.Value;
        json = JsonConvert.SerializeObject(his, Formatting.Indented);
        break;
}
Console.WriteLine(json);
Console.ReadLine();

The above generic method is just something I cobbled together in the last half hour, but I’m wondering if there is a more efficient way of creating these custom objects of mine, especially when each City has its own, different IEnumerable<Building> property.

EDIT

Here is the code I have for building the IEnumerable inside my GetData method:

public static IEnumerable<TBuilding> GetBuildingData<TBuilding>() 
    where TBuilding : Building, new()
{
    DataTable table;
    string myConnectionString = @"Data Source=(LocalDb)MSSQLLocalDb;Initial Catalog=Test;Integrated Security=True;";
    List<TBuilding> buildings = new List<TBuilding>();
    using (SqlConnection connection = new SqlConnection(myConnectionString))
    {
        using (SqlCommand command = new SqlCommand("[dbo].[GetListData]", connection) { CommandType = CommandType.StoredProcedure })
        {
            connection.Open();
            using (SqlDataAdapter adapter = new SqlDataAdapter(command))
            {
                table = new DataTable();
                adapter.Fill(table);
            }
            connection.Close();
        }
    }
    foreach (DataRow row in table.Rows)
    {
        TBuilding building = new TBuilding();
        building.Area = Convert.ToDouble(row["Area"]);
        building.Name = row["Name"].ToString();
        building.Stories = Convert.ToInt32(row["Stories"]);
        buildings.Add(building);
    }
    return buildings;
}

Solution

I will focus on..

Creating custom objects with custom properties using generics

Which you do in this method..

public static KeyValuePair<TCity, IEnumerable<TBuilding>> GetData<TCity, TBuilding>()
    where TCity : City, new()
    where TBuilding : Building, new()
{
    TCity city = new TCity();
    IEnumerable<TBuilding> buildings = new List<TBuilding>();
    return new KeyValuePair<TCity, IEnumerable<TBuilding>>(city, buildings);
}
  • A KeyValuePair is archaic, you can use a ValueTuple nowadays
  • TBuilding does not require the new() constraint in this method
  • The choice for IEnumerable over IList is questionable, since you retrieve the items later on and have no way of adding them to an IEnumerable. So why create the IEnumerable in this method?
  • The name should reflect what you are doing. CreateCity seems more appropriate.

I would refactor this method..

public static (TCity city, IList<TBuilding> buildings) CreateCity<TCity, TBuilding>()
    where TCity : City, new()
    where TBuilding : Building
{
    var city = new TCity();
    var buildings = new List<TBuilding>();
    return (city, buildings);
}

EDIT:

Thinking about the refactored method. This doesn’t even need to be a method anymore. It doesn’t do anything but create objects for the types you specify.

What I would really do is throw out this ancient way of ORM, and use an existing API (EF, NHibernate, Dapper).

public class MyBuilding : Building
{
    public int MyId { get; set; }
}

public class HisBuilding : Building
{
    public int HisId { get; set; }
}

Why do you have the id as properties of the subclasses, it’s a candidate for a base class member:

public class Building
{
  public int Id { get; set; }

If the subclasses must have specialized names for their Id property, then provide that as:

public class MyBuilding : Building
{
    public int MyId { get { return Id; } set { Id = value; } }
}

but that is a rather odd concept that you should avoid if possible.

The same holds for Cities.


public class MyCity : City
{
    public int MyId { get; set; }
    public IEnumerable<MyBuilding> Buildings { get; set; }
}

Normally you would have a materialized data set for buildings instead of an IEnumerable<T> – unless you’re creating the instances lazily/dynamically. You could maybe consider using a IReadonlyList<T> as type, if you don’t want it to be modifiable or else just a IList<T>


public static KeyValuePair<TCity, IEnumerable<TBuilding>> GetData<TCity, TBuilding>()
    where TCity : City, new()
    where TBuilding : Building, new()
{
    TCity city = new TCity();
    IEnumerable<TBuilding> buildings = new List<TBuilding>();
    return new KeyValuePair<TCity, IEnumerable<TBuilding>>(city, buildings);
}

If you changed the City base class to a generic like:

public class City<TBuilding> where TBuilding: Building
{
  public IList<TBuilding> Buildings { get; set; }
}

and dropped the buildings on the specialized cities, then you could return just the city from GetData, because you can initialize the Buildings property inside GetData, which I would rename to CreateCity()

public static TCity CreateCity<TCity, TBuilding>()
where TCity : City<TBuilding>, new()
where TBuilding : Building, new()
{
  TCity city = new TCity();
  city.Buildings = new List<TBuilding>();
  return city;
}

Ideally you could have a common baseclass for City and Building, because they share some significant properties like Id, Area and Name:

public abstract class AreaObject
{
  public int Id { get; set; }
  public string Name { get; set; }
  public double Area { get; set; }
}

A complete refactoring of your data model could then be:

public abstract class AreaObject
{
  public int Id { get; set; }
  public string Name { get; set; }
  public double Area { get; set; }
}

// For convenience a city base class without the generic type parameter:
public abstract class City : AreaObject
{
  public abstract IEnumerable<Building> GetBuildings();
}

public class City<TBuilding> : City where TBuilding : Building
{
  public IList<TBuilding> Buildings { get; set; }

  public override IEnumerable<Building> GetBuildings()
  {
    return Buildings;
  }
}

public class Building : AreaObject
{
  public int Stories { get; set; }
}

public class MyBuilding : Building
{
  public int MyId { get { return Id; } set { Id = value; } }
}

public class HisBuilding : Building
{
  public int HisId { get { return Id; } set { Id = value; } }
}

public class MyCity : City<MyBuilding>
{
  public int MyId { get { return Id; } set { Id = value; } }
}

public class HisCity : City<HisBuilding>
{
  public int HisId { get { return Id; } set { Id = value; } }
}

It’s a little odd to have two different sub cities having specialized buildings instead of just Buildings, but you may have reasons for that? (What if MyCity buys a building from HisCity can it then change from His to My?)


Here is the code I have for building the IEnumerable inside my
GetData method:

public static IEnumerable<TBuilding> GetBuildingData<TBuilding>()...

If that is going to be used inside GetData() shouldn’t it then take a city id as argument in order to minimize the query? If so you can change the GetData to:

public static TCity CreateCity<TCity, TBuilding>(int id)
where TCity : City<TBuilding>, new()
where TBuilding : Building, new()
{
  TCity city = new TCity();
  city.Buildings = GetBuildingData<TBuilding>(id);
  city.Id = id;
  return city;
}

And you’ll then have to modify your database query to only return the buildings for that city:

public static IList<TBuilding> GetBuildingData<TBuilding>(int cityId)
where TBuilding : Building, new() {...}

All in all this could simplify your use case to:

  int cityId = -1;
  string strInput = Console.ReadLine();
  int.TryParse(strInput, out cityId);
  if (cityId > 0)
  {
    City city = null;

    switch (cityId)
    {
      case 1:
        city = CreateCity<MyCity, MyBuilding>(cityId);
        break;
      case 2:
        city = CreateCity<HisCity, HisBuilding>(cityId);
        break;
      default:
        throw new InvalidOperationException("Invalid Id");
    }

    string json = JsonConvert.SerializeObject(city, Formatting.Indented);
    Console.WriteLine(json);
    Console.ReadLine();
  }

Update

As mentioned a couple of times in the above, the data model and class structure for especially City is rather unusual. A more conventional structure could be something like:

public abstract class AreaObject
{
  public int Id { get; set; }
  public string Name { get; set; }
  public double Area { get; set; }
}

public abstract class City : AreaObject
{
  public IList<Building> Buildings { get; set; }
}

public class MyCity : City
{
  // Uncomment if you really need this: public IList<MyBuilding> MyBuildings => Buildings.Where(b => b is MyBuilding).Cast<MyBuilding>().ToList();
}

public class HisCity : City
{
  // Uncomment if you really need this: public IList<HisBuilding> HisBuildings => Buildings.Where(b => b is HisBuilding).Cast<HisBuilding>().ToList();
}

public class Building : AreaObject
{
  public int Stories { get; set; }
}

public class MyBuilding : Building
{
}

public class HisBuilding : Building
{
}

As shown, City owns the Buildings property. The original concept of having specialized building properties per city sub class, doesn’t make much sense in real life, because what defines a building: its relation to a city or its materials, functionality etc.? Can’t two cities have a copy of the same building? And a MyCity should be able to Buy a building from HisCity etc. And a city should be able to own different types of building.

The factory method could then be simplified to this:

  public static TCity CreateCity<TCity>(int id)
    where TCity : City, new()
  {
    TCity city = new TCity();
    city.Buildings = GetBuildingData(id);
    city.Id = id;
    return city;
  }

  public static IList<Building> GetBuildingData(int cityId)
  {
    // The sql stuff...
  }

And the use case to:

  public static void Run()
  {
    int cityId = -1;
    string strInput = Console.ReadLine();
    int.TryParse(strInput, out cityId);
    if (cityId > 0)
    {
      City city = null;

      switch (cityId)
      {
        case 1:
          city = CreateCity<MyCity>(cityId);
          break;
        case 2:
          city = CreateCity<HisCity>(cityId);
          break;
        default:
          throw new InvalidOperationException("Invalid Id");
      }

      string json = JsonConvert.SerializeObject(city, Formatting.Indented);
      Console.WriteLine(json);
      Console.ReadLine();
    }
  }

Leave a Reply

Your email address will not be published. Required fields are marked *