I am building an applications in .net/c#/Entity Framework that uses a layered architecture. The applications interface to the outside world is a WCF service Layer. Underneath this layer I have the BL, Shared Library and the DAL.
Now, in order to make the business logic in my application testable, I am trying to introduce separation of concerns, loose coupling and high cohesion in order to be able to inject dependencies while testing.
I need some pointers as to if my approach described below is good enough or if I should be decoupling the code even further.
The following code snippet is used to query a database using dynamic linq. I need to use dynamic linq since I dont know the name of the table or the fields to query until runtime. The code first parses json parameters into types objects, then builds the query using these parameters, and finally the query is execute and the result is returned
Here is the GetData function that is used in the test below
IQueryHelper helper = new QueryHelper(Context.DatabaseContext);
//1. Prepare query
LinqQueryData queryData = helper.PrepareQueryData(filter);
//2. Build query
IQueryable query = helper.BuildQuery(queryData);
//3. Execute query
List<dynamic> dalEntities = helper.ExecuteQuery(query);
Here is the high level definion of the query helper class in DAL and its interface
public interface IQueryHelper
{
LinqQueryData PrepareQueryData(IDataQueryFilter filter);
IQueryable BuildQuery(LinqQueryData queryData);
List<dynamic> ExecuteQuery(IQueryable query);
}
public class QueryHelper : IQueryHelper
{
..
..
}
Here is the test that uses the logic as described above. The test constructor injects the mocked db into Context.DatabaseContext
[TestMethod]
public void Verify_GetBudgetData()
{
Shared.Poco.User dummyUser = new Shared.Poco.User();
dummyUser.UserName = "dummy";
string groupingsJSON = "[\"1\",\"44\",\"89\"]";
string valueTypeFilterJSON = "{1:1}";
string dimensionFilter = "{2:[\"200\",\"300\"],1:[\"3001\"],44:[\"1\",\"2\"]}";
DataQueryFilter filter = DataFilterHelper.GetDataQueryFilterByJSONData(
new FilterDataJSON()
{
DimensionFilter = dimensionFilter,
IsReference = false,
Groupings = groupingsJSON,
ValueType = valueTypeFilterJSON
}, dummyUser);
FlatBudgetData data = DataAggregation.GetData(dummyUser, filter);
Assert.AreEqual(2, data.Data.Count);
//min value for january and february
Assert.AreEqual(50, Convert.ToDecimal(data.Data.Count > 0 ? data.Data[0].AggregatedValue : -1));
}
To my questions
- Is this Business layer logic "good enough" or what more can be done to achieve loose coupling, high cohesion and testable code?
- Should I inject the data context to query in the constructor? Note that the QueryHelper definitions is located in DAL. The code
that uses it is located in BL
Please let me know if I should post additional code for clarity. I'm mostly interested if the interface IQueryHelper is sufficient..
I generally use IServices, Services, and MockServices.
- IServices provides the available operations that all business logic must invoke methods on.
- Services is the data access layer that my code-behind injects into view-model (i.e. actual database).
- MockServices is the data access layer that my unit tests injects to the view-model (i.e. mock data).
IServices:
public interface IServices
{
IEnumerable<Warehouse> LoadSupply(Lookup lookup);
IEnumerable<Demand> LoadDemand(IEnumerable<string> stockCodes, int daysFilter, Lookup lookup);
IEnumerable<Inventory> LoadParts(int daysFilter);
Narration LoadNarration(string stockCode);
IEnumerable<PurchaseHistory> LoadPurchaseHistory(string stockCode);
IEnumerable<StockAlternative> LoadAlternativeStockCodes();
AdditionalInfo GetSupplier(string stockCode);
}
MockServices:
public class MockServices : IServices
{
#region Constants
const int DEFAULT_TIMELINE = 30;
#endregion
#region Singleton
static MockServices _mockServices = null;
private MockServices()
{
}
public static MockServices Instance
{
get
{
if (_mockServices == null)
{
_mockServices = new MockServices();
}
return _mockServices;
}
}
#endregion
#region Members
IEnumerable<Warehouse> _supply = null;
IEnumerable<Demand> _demand = null;
IEnumerable<StockAlternative> _stockAlternatives = null;
IConfirmationInteraction _refreshConfirmationDialog = null;
IConfirmationInteraction _extendedTimelineConfirmationDialog = null;
#endregion
#region Boot
public MockServices(IEnumerable<Warehouse> supply, IEnumerable<Demand> demand, IEnumerable<StockAlternative> stockAlternatives, IConfirmationInteraction refreshConfirmationDialog, IConfirmationInteraction extendedTimelineConfirmationDialog)
{
_supply = supply;
_demand = demand;
_stockAlternatives = stockAlternatives;
_refreshConfirmationDialog = refreshConfirmationDialog;
_extendedTimelineConfirmationDialog = extendedTimelineConfirmationDialog;
}
public IEnumerable<StockAlternative> LoadAlternativeStockCodes()
{
return _stockAlternatives;
}
public IEnumerable<Warehouse> LoadSupply(Lookup lookup)
{
return _supply;
}
public IEnumerable<Demand> LoadDemand(IEnumerable<string> stockCodes, int daysFilter, Syspro.Business.Lookup lookup)
{
return _demand;
}
public IEnumerable<Inventory> LoadParts(int daysFilter)
{
var job1 = new Job() { Id = Globals.jobId1, AssembledRequiredDate = DateTime.Now, StockCode = Globals.stockCode100 };
var job2 = new Job() { Id = Globals.jobId2, AssembledRequiredDate = DateTime.Now, StockCode = Globals.stockCode200 };
var job3 = new Job() { Id = Globals.jobId3, AssembledRequiredDate = DateTime.Now, StockCode = Globals.stockCode300 };
return new HashSet<Inventory>()
{
new Inventory() { StockCode = Globals.stockCode100, UnitQTYRequired = 1, Category = "Category_1", Details = new PartDetails() { Warehouse = Globals.Instance.warehouse1, Job = job1} },
new Inventory() { StockCode = Globals.stockCode200, UnitQTYRequired = 2, Category = "Category_1", Details = new PartDetails() { Warehouse = Globals.Instance.warehouse1, Job = job2} },
new Inventory() { StockCode = Globals.stockCode300, UnitQTYRequired = 3, Category = "Category_1", Details = new PartDetails() { Warehouse = Globals.Instance.warehouse1, Job = job3} },
};
}
#endregion
#region Selection
public Narration LoadNarration(string stockCode)
{
return new Narration()
{
Text = "Some description"
};
}
public IEnumerable<PurchaseHistory> LoadPurchaseHistory(string stockCode)
{
return new List<PurchaseHistory>();
}
public AdditionalInfo GetSupplier(string stockCode)
{
return new AdditionalInfo()
{
SupplierName = "Some supplier name"
};
}
#endregion
#region Creation
public Inject Dependencies(IEnumerable<Warehouse> supply, IEnumerable<Demand> demand, IEnumerable<StockAlternative> stockAlternatives, IConfirmationInteraction refreshConfirmation = null, IConfirmationInteraction extendedTimelineConfirmation = null)
{
return new Inject()
{
Services = new MockServices(supply, demand, stockAlternatives, refreshConfirmation, extendedTimelineConfirmation),
Lookup = new Lookup()
{
PartKeyToCachedParts = new Dictionary<string, Inventory>(),
PartkeyToStockcode = new Dictionary<string, string>(),
DaysRemainingToCompletedJobs = new Dictionary<int, HashSet<Job>>(),
.
.
.
},
DaysFilterDefault = DEFAULT_TIMELINE,
FilterOnShortage = true,
PartCache = null
};
}
public List<StockAlternative> Alternatives()
{
var stockAlternatives = new List<StockAlternative>() { new StockAlternative() { StockCode = Globals.stockCode100, AlternativeStockcode = Globals.stockCode100Alt1 } };
return stockAlternatives;
}
public List<Demand> Demand()
{
var demand = new List<Demand>()
{
new Demand(){ Job = new Job{ Id = Globals.jobId1, StockCode = Globals.stockCode100, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode100, RequiredQTY = 1},
new Demand(){ Job = new Job{ Id = Globals.jobId2, StockCode = Globals.stockCode200, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode200, RequiredQTY = 2},
};
return demand;
}
public List<Warehouse> Supply()
{
var supply = new List<Warehouse>()
{
Globals.Instance.warehouse1,
Globals.Instance.warehouse2,
Globals.Instance.warehouse3,
};
return supply;
}
#endregion
}
Services:
public class Services : IServices
{
#region Singleton
static Services services = null;
private Services()
{
}
public static Services Instance
{
get
{
if (services == null)
{
services = new Services();
}
return services;
}
}
#endregion
public IEnumerable<Inventory> LoadParts(int daysFilter)
{
return InventoryRepository.Instance.Get(daysFilter);
}
public IEnumerable<Warehouse> LoadSupply(Lookup lookup)
{
return SupplyRepository.Instance.Get(lookup);
}
public IEnumerable<StockAlternative> LoadAlternativeStockCodes()
{
return InventoryRepository.Instance.GetAlternatives();
}
public IEnumerable<Demand> LoadDemand(IEnumerable<string> stockCodes, int daysFilter, Lookup lookup)
{
return DemandRepository.Instance.Get(stockCodes, daysFilter, lookup);
}
.
.
.
Unit Test:
[TestMethod]
public void shortage_exists()
{
// Setup
var supply = new List<Warehouse>() { Globals.Instance.warehouse1, Globals.Instance.warehouse2, Globals.Instance.warehouse3 };
Globals.Instance.warehouse1.TotalQty = 1;
Globals.Instance.warehouse2.TotalQty = 2;
Globals.Instance.warehouse3.TotalQty = 3;
var demand = new List<Demand>()
{
new Demand(){ Job = new Job{ Id = Globals.jobId1, StockCode = Globals.stockCode100, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode100, RequiredQTY = 1},
new Demand(){ Job = new Job{ Id = Globals.jobId2, StockCode = Globals.stockCode200, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode200, RequiredQTY = 3},
new Demand(){ Job = new Job{ Id = Globals.jobId3, StockCode = Globals.stockCode300, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode300, RequiredQTY = 4},
};
var alternatives = _mock.Alternatives();
var dependencies = _mock.Dependencies(supply, demand, alternatives);
var viewModel = new MainViewModel();
viewModel.Register(dependencies);
// Test
viewModel.Load();
AwaitCompletion(viewModel);
// Verify
var part100IsNotShort = dependencies.PartCache.Where(p => (p.StockCode == Globals.stockCode100) && (!p.HasShortage)).Single() != null;
var part200IsShort = dependencies.PartCache.Where(p => (p.StockCode == Globals.stockCode200) && (p.HasShortage)).Single() != null;
var part300IsShort = dependencies.PartCache.Where(p => (p.StockCode == Globals.stockCode300) && (p.HasShortage)).Single() != null;
Assert.AreEqual(true, part100IsNotShort &&
part200IsShort &&
part300IsShort);
}
CodeBehnd:
public MainWindow()
{
InitializeComponent();
this.Loaded += (s, e) =>
{
this.viewModel = this.DataContext as MainViewModel;
var dependencies = GetDependencies();
this.viewModel.Register(dependencies);
.
.
.
ViewModel:
public MyViewModel()
{
.
.
.
public void Register(Inject dependencies)
{
try
{
this.Injected = dependencies;
this.Injected.RefreshConfirmation.RequestConfirmation += (message, caption) =>
{
var result = MessageBox.Show(message, caption, MessageBoxButton.YesNo, MessageBoxImage.Question);
return result;
};
this.Injected.ExtendTimelineConfirmation.RequestConfirmation += (message, caption) =>
{
var result = MessageBox.Show(message, caption, MessageBoxButton.YesNo, MessageBoxImage.Question);
return result;
};
.
.
.
}
catch (Exception ex)
{
Debug.WriteLine(ex.GetBaseException().Message);
}
}