How to search sub-classes using the repository pattern?
I have several sub-classes of a payment transaction base type (credit card, check, cash, billMeLater, etc). Each sub-class has it's own repository since each has its own properties and way of being fetched. I need to be able to search payment transactions and I've gone down a road that has ended up causing more headaches. The trick is that sometimes the client needs to search on common properties like amount or customer name AND sometimes the client needs to search on payment-type-specific properties like credit card number or bank routing number... but the domain-level search method needs to be able to return all types of the transaction base.
I have the following levels of abstraction:
WCF Layer with a SearchTransactions() method.
Domain layer with a SearchTransactions() method.
Data layer with multiple repositories, each with a Search() method (payment type specific).
Database (via EF) with typical DBA-required unintelligible mess
How would you do this?
EDIT:
For added context, here are some examples of possible payment types and their base:
public abstract class TransactionBase
{
public int TransactionId { get; set; }
public decimal Amount { get; set; }
}
public class CreditCardTransaction : TransactionBase
{
public string CardNumber { get; set; }
public int ExpirationMonth { get; set; }
public int ExpirationYear { get; set; }
}
public class CheckTransaction : TransactionBase
{
public string BankAccountNumber { get; set; }
public string RoutingNumber { get; set; }
}
So, the client should be able to search on CardNumber, RoutingNumber, Amount, etc., all from one method. If the client searches on Amount (param on the base), the method should return both CreditCardTransaction's and CheckTransaction's. If the client searches on BankAccountNumber, it should only return CheckTransactions.
CLIENT REQUIREMENTS and EARLIER SOLUTION:
The client requires that there be one call to search multiple transaction types. The client doesn't care much what they pass in as arguments unless they require more than one call to cover all payment types. One idea I had earlier on was to use classes that carried search criteria. Then I could have sub-classes of the search criteria classes that searched the more specific payment type properties. Lik开发者_如何学Ce this:
public class TransactionSearchCriteriaBase
{
public int TransactionId { get; set; }
public decimal Amount { get; set; }
}
public class CreditCardTransactionSearchCriteria : TransactionSearchCriteriaBase
{
public string CardNumber { get; set; }
public int ExpirationMonth { get; set; }
public int ExpirationYear { get; set; }
}
So, if the client wants to search on common properties like Amount, they pass in the TransactionSearchCriteriaBase. If they pass in the CreditCardTransactionSearchCriteria, they end up searching credit card transactions. For instance:
var listOfTransactions = _transactionService.Search(new CreditCardTransactionSearchCriteria{ Amount = 10m, CardNumber = "1111" });
I replaced the almost-inevitable switch/if block with a repository factory that handed back a list of applicable repositories based on the type of criteria object passed in to the factory.
The rabbit hole goes deeper. I'd like less of a rabbit hole.
ANOTHER PIECE OF INFO:
Since we're doing this in EF 3.5, we don't have POCO support. So, we don't consider the objects that EF generates as domain objects. Our repositories map the various disconnected EF objects to domain objects and returns them to the domain services that call them.
I would rethink your Entity Framework model.
That domain model you have provided looks perfect for Table-per-type Inheritance.
Then you could use the LINQ .OfType<T>()
method to filter different transaction types, based on a generic type parameter on your Repository:
public class TransactionRepository : IRepository<Transaction>
{
public TTransaction Find<TTransaction>(int transactionId)
where TTransaction : TransactionBase, new()
{
return _ctx.Transactions.OfType<TTransaction>().SingleOrDefault(tran => tran.TransactionId == transactionId);
}
}
Then you could do this:
var ccTran = _repository.Find<CreditCardTransaction>(x => x.TransactionId == 1);
var cTran = _repository.Find<CheckTransaction>(x => x.TransactionId == 2);
As for your questions:
So, the client should be able to search on CardNumber, RoutingNumber, Amount, etc., all from one method
I don't think that's possible with one method, well certainly not without some kind of if/switch statement.
The kicker is the filtering - it all comes down to the signature of the repository method - what is provided, what generic constraints, etc.
If your saying each sub-type has it's own repository, then does it really make sense to have a single method which serves all three repositories? Where should this magical method live?
So overall, i think you've reached the point many have reached, where your domain is fighting against Entity Framework.
Basically, if your working on an objectset of type AbstractA, you can't "downcast" to a objectset of type DerivedA in order to do the filtering.
It comes down to how much of your domain model your willing to compromise. I myself had a similar problem, and i ended up using TPT inheritance (then switching to TPH because the performance was better).
So without knowing too much about your domain other than what you've mentioned, i think you need to re-think the statement "Each sub-class has it's own repository since each has its own properties and way of being fetched".
It looks like you should have a single repository, TPT/TPH in your EF model, and the "search" method on your Repository with a generic type constraint on the transaction type.
If your gun-ho on having the magic single method, you would need a nasty switch/if statement, and delegate the filtering to a specific method.
In DDD the main goal of an Aggregate is to maintain and manage consistency. If I follow your example correctly, you have two types of Aggregates - each one represented by Aggregate Roots of CreditCardTransaction and CheckTransaction.
However the scenario that you've described has nothing to do with maintaining consistency as it doesn't change any data. What you want to achieve is to provide a kind of report to a user. So instead of trying to bend Aggregates, I would introduce another repository - TransactionRepository with a single method of FindTransaction(TransactionQuery). This repo would exist for a single reason only - to query your database for the data you require to show to user (yes, it would be a readonly repository).
In other words I would suggest using your Aggregates and domain entities when you perform some operations that actually change the data but not for queries that only show the data to users - use simpler objects instead (plus you can aggregate the data without messing with your domain structure, just like in your example).
精彩评论