Skip to content

Instantly share code, notes, and snippets.

@CallumVass
Last active January 2, 2016 11:59
Show Gist options
  • Save CallumVass/8300400 to your computer and use it in GitHub Desktop.
Save CallumVass/8300400 to your computer and use it in GitHub Desktop.
Hacky way to load the correct service for before/after save operations to provide additional validation / business logic to entities for Breeze JS
public class SalesAppContextProvider : EFContextProvider<SalesAppContext>
{
private readonly ServiceFactory _serviceFactory;
public SalesAppContextProvider(ServiceFactory serviceFactory)
{
_serviceFactory = serviceFactory;
}
protected override bool BeforeSaveEntity(EntityInfo entityInfo)
{
var entityType = entityInfo.Entity.GetType();
var service = _serviceFactory.GetServiceFor(entityType);
switch (entityInfo.EntityState)
{
case EntityState.Added:
return service.BeforeSaveEntityAdded(entityInfo.Entity);
case EntityState.Modified:
return service.BeforeSaveEntityModified(entityInfo.Entity);
case EntityState.Deleted:
return service.BeforeSaveEntityDeleted(entityInfo.Entity);
default:
return true;
}
}
protected override void AfterSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap, List<KeyMapping> keyMappings)
{
foreach (var map in saveMap)
{
var entityType = map.Key;
var service = _serviceFactory.GetServiceFor(entityType);
foreach (var entityInfo in map.Value)
{
switch (entityInfo.EntityState)
{
case EntityState.Added:
service.AfterSaveEntityAdded(entityInfo.Entity);
break;
case EntityState.Modified:
service.AfterSaveEntityModified(entityInfo.Entity);
break;
case EntityState.Deleted:
service.AfterSaveEntityDeleted(entityInfo.Entity);
break;
}
}
}
base.AfterSaveEntities(saveMap, keyMappings);
}
}
public class CustomerService : IService
{
private readonly IMailService _mailService;
public CustomerService(IMailServce mailService) {
_mailService = mailService;
}
public bool BeforeSaveEntityAdded(object entity)
{
return true;
}
public bool BeforeSaveEntityModified(object entity)
{
return true;
}
public bool BeforeSaveEntityDeleted(object entity)
{
return true;
}
public void AfterSaveEntityAdded(object entity)
{
var customer = entity as Customer;
_mailService.NewCustomerNotification(customer);
}
public void AfterSaveEntityModified(object entity)
{
}
public void AfterSaveEntityDeleted(object entity)
{
}
}
public class ServiceFactory
{
private readonly IMailService _mailService;
public ServiceFactory(IMailService mailService) {
_mailService = mailService;
}
private readonly Dictionary<Type, int> _typeDictionary = new Dictionary<Type, int>
{
{ typeof(Collection), 0 },
{ typeof(Customer), 1 }
};
public IService GetServiceFor(Type entityType)
{
switch (_typeDictionary[entityType])
{
case 0:
return new CollectionService();
case 1:
return new CustomerService(_mailService);
default:
throw new ArgumentException("entityType");
}
}
}
@wardbell
Copy link

wardbell commented Jan 7, 2014

I think it's a fine and flexible start. Doesn't look hacky to me.

A few observations:

  • no need to call the base method of any of the injectors as they are empty; could just do this

    return service.BeforeSaveOperation(entityInfo.Entity);

  • I prefer beforeSaveEntities because

    • it facilitates cross-entity validation
    • cancel the save if an invalid EntityType in the change-set
    • you can choose to evaluate all entities of the same type if you need to
    • you can choose the order of evaluation
    • you can manipulate the SaveMap itself
  • I would provide the SaveMap itself as a parameter to your save operations

  • I foresee useful extensions for

    • registering services in the factory elsewhere
    • injecting factory services based on what this user is allowed to do
    • perhaps different ServiceFactories or configurations for different SaveChanges endpoints if I had them (see "Named Saves"
  • Rename loadServiceFor as getServiceFor so I don't have a heart attack thinking that you'll be re-fetching and re-initializing the type-specific services over and over again, twice per entity in the change-set

This makes a lot of sense to me.

What are you worried about?

@CallumVass
Copy link
Author

I guess the bit that felt "hacky" to me was the type dictionary, and having to add a new one in each time I add a new service for an Entity. Also, the new'ing up of the concrete implementation rather than in the IoC container, which might be better perhaps for handling any dependencies the services may have? But yes, overall I'm quite pleased with it. Thanks for your comment, it's really appreciated! I will take those points on board and re-factor a bit more! :)

@wardbell
Copy link

wardbell commented Jan 7, 2014

Yes, for sure you'd inject rather than new up the factory. You could get fancy and use MEF to have type-specific services self-register. But you're doing the most important thing which is factoring the business logic so that it isn't all lumped in a gigantic before/after interceptor and your factoring is both rational and flexible.

I've done such factoring myself in DocCode but I like your approach better and will probably steal it in a future DocCode update (I'll credit you and this gist).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment