If you've seen my twitter feed lately (if you haven't, I don't blame you), I've been complaining a LOT about datasets, so I went looking to an old friend, subsonic. Why subsonic? Because it works. It works well. It works with just about every database I've EVER come across. In my opinion, its a true abstraction layer between data and service to which it fits nicely in between because it works with so much stuff.
As of late, I've been taking Jon's suggestion of highly-specialized interfaces -- GetService, UpdateService, DeleteService and CreateService to make up a one stop shop to make this work. I know he didn't invent this idea, but he brought it to my attention in a very indirect way. Anyway, I've found this to be a great idea, but it's amazingly difficult to imbue on certain data layers, such as with datasets. I WAS able to use a IGetService for it, as such...
public class GetService<T> : IGetService<T> where T : class, IObjectToEntity, IDataTableEntity
{
private DataTable _table;
private SuperAdapter _adapter;
private OracleDataAdapter _dbAdapter;
private IQueryable<T> _entityList;
public GetService(DataTable table)
{
_table = table;
_adapter = new SuperAdapter();
_dbAdapter = _adapter.AdapterFactory(_table.TableName);
_dbAdapter.Fill(_table);
_entityList = _table.Rows.ToEntityListOf<T>().AsQueryable();
}
}
With an extension method of .ToEntityListOf<T>, this became an awesome way to convert the horrid datasets into a more manageable object to get things like, oh I don't know, IDs and other useful data out, easily. Each entity was given a constructor that parsed the datarow into the object ... nice hu? Unfortunately, this is where this ended. It was near-impossible (not worth the time) to get the Delete/Create/Update to work so I abandoned it since at LEAST I was able to see the base data. As heavy as this operation was/is, it gave me a bit more control over the data.
Then I had to clean up the data in the database... something along the lines of 2000 records scattered all over the place -- 4 tables, 3 lookup tables namely. I went looking for subsonic. I asked the same question "can I use the individual get/update/etc services?" and you CAN.
My typical GetObjectService does the usuals. Gets by Id, Gets by an expression or just gets everything. That's not hard. What CAN be hard is figuring out how to get whatever data layer you're speaking though, to do what you want, how you want it. I'm using subsonic 2, so there's no linq ... that doesn't mean it can't work with it. "Well, you could use the Subsonic.Query" and you'd be right, but I don't want to use anything more than absolutely necessary, I'm driving for core simplicity. This can make things a bit ugly but not less maintainable. Digging into subsonic a little, a lot of things worked around the ActiveRecord object and gave me a sense of where to start... and that's where it ended -- take this for example, my get by expression ...
public IQueryable<TEntity> Get(System.Linq.Expressions.Expression<Func<TEntity, bool>> expression)
{
var collection = new TCollection();
collection.LoadAndCloseReader(ActiveRecord<TEntity>.FetchAll());
return collection.AsQueryable().Where(expression);
}
Yes that last line is kinda ugly ... but it gets the job done and is not hard to figure out. EXACTLY what I'm looking for. If I move in another datalayer, what are the chances the manipulation would be the same in some way? Pretty high, so I'll take it.
Ok, so far so good, but so what? Gotta test that right? Yes, and that's a bit easier to do than you might think. Using Moq, I came up with a "FakeGetObjectFactory" ... looks like this.
private Mock<IGetObjectService<TEntity>> FakeGetServiceFactory<TEntity>(List<TEntity> fakeList) where TEntity : ActiveRecord<TEntity>, new()
{
var mockGet = new Mock<IGetObjectService<TEntity>>();
mockGet.Setup(mock => mock.GetAll()).Returns(fakeList);
mockGet.Setup(mock => mock.Get(It.IsAny<int>())).Returns((int i) => fakeList.Find(fake => fake.GetPrimaryKeyValue().ToString() == i.ToString()));
mockGet.Setup(mock => mock.Get(It.IsAny<Expression<Func<TEntity, bool>>>())).Returns((Expression<Func<TEntity, bool>> expression) => fakeList.AsQueryable().Where(expression));
return mockGet;
}
In english, the "It.IsAny<>" says exactly that -- if its any int, do the following stuff. Using this is quite easy, since you're tossing in a List<TEntity> (an in-mem repository if you will) as a parameter ...
_mockFakeGetService = FakeGetServiceFactory(_fakeList);
_getFakeListService = _mockFakeGetService.Object;
Done and done! Now my _getFakeListService acts like it does everywhere else AND my code is testable now! The bigger bonus is later on, if the client decides to move it to sql (99.999999% likely), they can, and they have a place to do this.