Disclaimer: Below is my very simplified description of a problem. Please while reading it imagine some complicated modular application (ERP, Visual Studio, Adobe Photoshop) that is evolving over the years during which its features will be added and removed in the hope that it will not end up in spaghetti code.
Let say I have following entity and corresponding table in database
class Customer
{
public int Id { get; set; }
}
then I will use some ORM, build DataContext and create my application
static void Main(string[] args)
{
//Data Layer
var context = new DataContext();
IQueryable<Customer> customers = context.Customers;
//GUI
foreach (var customer in customers)
foreach (var property in customer.GetType().GetProperties())
Console.WriteLine($"{property.Name}:{property.GetValue(customer)}");
}
Application is done, my customer is happy, case closed.
Half year later my customer asks me to add Name to the Customer. I want to do it without touching previous code.
So first I will create new entity and corresponding table in database and add it in ORM (please ignore the fact that I'm modifying the same DataContext, this is easy to fix)
class CustomerName
{
public int Id { get; set; }
public string Name { get; set; }
}
CustomerName will add new property to Customer but to have complete Customer information we need to join them together, so let's try to modify our application without touching previous code
static void Main(string[] args)
{
//Data Layer
var context = new DataContext();
IQueryable<Customer> customers = context.Customers;
//new code that doesn't even compile
customers = from c in customers
join cn in context.CustomerNames on c.Id equals cn.Id
select new {c, cn}; //<-- what should be here??
//GUI
foreach (var customer in customers)
foreach (var property in customer.GetType().GetProperties())
Console.WriteLine($"{property.Name}:{property.GetValue(customer)}");
}
As you can see I have no idea to what map my information from join so that it still will be a valid Customer object.
And no, I cannot use inheritance.
Why?
Because at the same time another developer can be asked for functionality to block customer and create following entity:
class BlockedCustomer
{
public int Id { get; set; }
public bool Blocked { get; set; }
}
He will not know anything about Customer Name, therefore he may only depend on Customer, and at runtime our both features will result in something like this:
static void Main(string[] args)
{
//Data Layer
var context = new DataContext();
IQueryable<Customer> customers = context.Customers;
//new code that doesn't even compile
customers = from c in customers
join cn in context.CustomerNames on c.Id equals cn.Id
select new {c, cn}; //<-- what should be here??
customers = from c in customers
join b in context.BlockedCustomers on c.Id equals b.Id
select new { c, b }; //<-- what should be here??
//GUI
foreach (var customer in customers)
foreach (var property in customer.GetType().GetProperties())
Console.WriteLine($"{property.Name}:{property.GetValue(customer)}");
}
I have 2 ideas how to solve it
- Create some container class that will inherit from Customer and play with casting/converting it to CustomerName or BlockedCustomer when needed.
Something like this:
class CustomerWith<T> : Customer
{
private T Value;
public CustomerWith(Customer c, T value) : base(c)
{
Value = value;
}
}
and then
customers = from c in customers
join cn in context.CustomerNames on c.Id equals cn.Id
select new CustomerWith<CustomerName>(c, cn);
- Use ConditionalWeakTable to store (at data layer level) CustomerName and BlockedCustomer associated with Customer and modify (once) UI to be aware of such things
As to my knowledge, both solutions unfortunately require me to write my own LINQ mapper (including change tracking) and I want to avoid it.
- Do you know any ORM that know how to handle such requirements?
- Or maybe there is much better/simpler solution to write applications and don't violate Open/Closed principle?
Edit - Some clarifications after comments:
- I'm talking about properties that have one-to-one relationship with Customer. Usually such properties are added as additional columns in the table.
- I want to send only one SQL query to database. So adding 20 such new features/properties/columns shouldn't end up in 20 queries.
- What I showed was a simplified version of application I have in mind. I'm thinking about app which dependencies will be structured in the following way [UI]-->[Business Logic]<--[Data]. All 3 should be open for extension and closed for modification but in this question I'm focusing on [Data] layer. [Business Logic] will ask [Data] layer (using Linq) for Customers. So even after extending it, Customer BL will just ask for Customer (in Linq), but extension to [Business Logic] will need CustomerName. Question is how to extend [Data] Layer so that it will still return Customer to Customer BL, but provide CustomerName to Customer name BL and still send one query to database.
- When I showed Joins as a proposed solution, it may be not clear that they will be not hard coded in [Data] layer, but rather [Data] layer should know that it should call some methods that may want to extend the query and which will be registered by main() module. Main() module is the only one that knows about all dependencies and for the purpose of this question it's not important how.