After recently getting a bulk of work done on my WebAPI service layer, I thought I'd post some of my working on here for you guys to tear apart, generally, most of it 'feels' mostly okay, but I know there are areas for improvement. Hopefully no screaming issues with it:
Someone is going to quote me Ayende, note that my queries are separated and passed in, I'm not creating countless methods.
Hopefully you will see that there is some use in the abstraction I've put in place, since some of the objects don't map nicely to EF objects and there is some pretty complex logic that needs to executed in different conditions, so duplication would be an issue without it I believe.
My routing:
config.Routes.MapHttpRoute(
name: ControllerOnly,
routeTemplate: "{controller}"
);
//For filtering and lookups we will overload the get method to pass in more parameters.
config.Routes.MapHttpRoute(
name: ControllerAndAction,
routeTemplate: "{controller}/act/{action}/",
defaults: null//, //defaults: new { id = RouteParameter.Optional } //,
//constraints: new { id = @"^\d+$" } // id must be all digits
);
config.Routes.MapHttpRoute(
name: ControllerAndActionId,
routeTemplate: "{controller}/act/{action}/{id}",
defaults: null//, //defaults: new { id = RouteParameter.Optional } //,
//constraints: new { id = @"^\d+$" } // id must be all digits
);
//For filtering and lookups we will overload the get method to pass in more parameters.
config.Routes.MapHttpRoute(
name: ControllerAndId,
routeTemplate: "{controller}/{id}",
defaults: null//, //defaults: new { id = RouteParameter.Optional } //,
//constraints: new { id = @"^\d+$" } // id must be all digits
);
This routing is a bit crusty, since I have to use /act/ to call an action, since the id
could be a string and the routing was matching the action name to id
. Any alternatives welcomed.
public class FaqController : ControllerBase
{
private readonly IFaqRepository _repo;
public FaqController(IFaqRepository repo, IUnitOfWork uow, ITokenRepository tokens)
{
_uow = uow;
_repo = repo;
_tokens = tokens;
}
// [Authorize]
[UnitOfWorkCommit]
public void Post(FaqContent content)
{
if (content.FaqId != 0)
{
_repo.Update(content, content.FaqId);
}
else
{
_repo.Insert(content);
}
}
[UnitOfWorkCommit]
[HttpDelete]
public void Delete(int id) {
_repo.Delete(id);
}
[Authorize]
public IEnumerable<FaqContent> Get()
{
return _repo.Get(orderBy: o => o.OrderBy(i => i.FaqId));
}
[Authorize]
public IEnumerable<FaqContent> Get(int id)
{
return _repo.Get(filter: j => j.FaqId == id, orderBy: o => o.OrderBy(i => i.FaqId));
}
}
A few basic CRUD operations above, using proper HTTP verbs and delegating down to repository. Note the authorize attribute.
public class AuthHandler : DelegatingHandler
{
private IUserRepository _userRepository;
private ITokenRepository _tokenRepo;
public AuthHandler()
{
}
protected Guid? GetTokenValue(HttpRequestMessage request)
{
Guid tokenValue;
var accessToken = request.Headers.GetCookies("token"); //See if the token is in the cookies
if (accessToken.Count == 0) //Nothing in the cookie... Check the querystring
{
var qs = request.GetQueryNameValuePairs();
var accessTokenQs = qs.Where(o => o.Key == "token").ToList();
if (accessTokenQs.Count == 0)
{
return null;
}
else
{
if (Guid.TryParse(accessTokenQs[0].Value, out tokenValue))
return tokenValue;
return null;
}
}
else
{
if (Guid.TryParse(accessToken[0]["token"].Value, out tokenValue))
return tokenValue;
return null;
}
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_userRepository = request.GetDependencyScope().GetService(typeof(IUserRepository)) as IUserRepository;
_tokenRepo = request.GetDependencyScope().GetService(typeof(ITokenRepository)) as ITokenRepository;
var tokenValue = GetTokenValue(request);
if (tokenValue == null)
{
return base.SendAsync(request, cancellationToken); //Still no token, carry on
}
var token = _tokenRepo.CheckToken(tokenValue.Value, true);
if (token == null)
return base.SendAsync(request, cancellationToken);
var user = _userRepository.GetByUserName(token.AssignedToUser);
var identity = new GenericIdentity(user.UserName, "Basic");
var principal = new GenericPrincipal(identity, user.Roles.Select(o => o.RoleName).ToArray());
Thread.CurrentPrincipal = principal;
return base.SendAsync(request, cancellationToken);
}
}
This is what handles the authorization, I am using cached repositories at the moment for stuff like that, so I don't hit the DB every time, these use the MemoryCache to store result sets, currently they have a sliding timer but I'll need to rework that at the moment as other applications will change the data, so I need to build in some contingency.
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
internal IUnitOfWork context;
internal DbSet<TEntity> dbSet;
private List<IAction<TEntity>> _actionsOnSave = new List<IAction<TEntity>>();
public ObservableCollection<TEntity> GetLocalEntities()
{
return dbSet.Local;
}
public void FireActions()
{
foreach (var act in _actionsOnSave)
{
act.Execute(this);
}
//Execute each action
}
public void AddAction(IAction<TEntity> action)
{
_actionsOnSave.Add(action);
}
public GenericRepository(IUnitOfWork uow)
{
this.context = uow;
this.dbSet = context.GetContext().Set<TEntity>();
}
public virtual IQueryable<TEntity> GetLazy(
Expression<Func<TEntity, bool>> filter = null, DynamicFilter dynamicFilters = null,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
if (dynamicFilters != null)
{
query = dynamicFilters.FilterObjectSet(query);
}
return query;
}
public object GetPagedData(int pageNo, int rows, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy, DynamicFilter f)
{
//Add back in order by to optimize perforamnce on large DS
var startAt = (pageNo * rows) - rows;
var retrieved = GetLazy(dynamicFilters: f); //Get an instance of IQueryable to use for the class
var totalRows = retrieved.Count(); //Count before paging occurs
int totalPages = (int)Math.Ceiling((double)totalRows / (double)rows);
if (pageNo > totalPages) pageNo = totalPages;
var filteredResults = ApplyPaging(retrieved, orderBy, startAt, rows);
var ret = new
{
page = pageNo,
total = totalPages,
records = totalRows,
repeatitems = true,
cell = "cell",
userdata = "userdata",
rows = filteredResults
};
return ret;
}
public virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity> query, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, int skip = 0, int take = 0)
{
if (orderBy != null)
{
if (take != 0)
{
return orderBy(query).Skip(skip).Take(take);
}
else
{
return orderBy(query).Skip(skip);
}
}
return query;
}
public virtual IEnumerable<TEntity> Get(
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null)
{
IQueryable<TEntity> query = dbSet;
if (orderBy != null)
{
return orderBy(query).ToList();
}
throw new Exception("You must specify an OrderBy");
//return null;
}
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
int skip = 0, int take = 0,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
if (orderBy != null)
{
if (take != 0)
{
return orderBy(query).Skip(skip).Take(take).ToList();
}
else
{
return orderBy(query).Skip(skip).ToList();
}
}
throw new Exception("You must specify an OrderBy");
//return null;
}
public virtual int GetCount(
Expression<Func<TEntity, bool>> filter = null)
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
return query.Count();
}
public virtual TEntity GetByID(object id)
{
return dbSet.Find(id);
}
public virtual void Insert(TEntity entity)
{
dbSet.Add(entity);
}
public virtual void Delete(object id)
{
TEntity entityToDelete = dbSet.Find(id);
if (entityToDelete == null)
{
throw new Exception(string.Format("Entity with ID ({0}) not found in dbSet", id));
}
Delete(entityToDelete);
}
public virtual void Delete(TEntity entityToDelete)
{
if (context.GetContext().Entry(entityToDelete).State == EntityState.Detached)
{
dbSet.Attach(entityToDelete);
}
dbSet.Remove(entityToDelete);
}
public virtual void Update(TEntity entityToUpdate, object id)
{
if (entityToUpdate == null)
{
throw new ArgumentException("Cannot add a null entity.");
}
var entry = context.GetContext().Entry(entityToUpdate);
if (entry.State == EntityState.Detached)
{
var ent = dbSet.Find(id);
if (ent != null)
{
var attachedEntry = context.GetContext().Entry(ent);
attachedEntry.CurrentValues.SetValues(entityToUpdate);
}
else
{
entry.State = EntityState.Modified;
}
}
}
}
My generic repo, I have concrete implementations as well, where I might need to return a more complex object.
Finally, my unit of work filter:
public class UnitOfWorkCommitAttribute :ActionFilterAttribute
{
public IUnitOfWork uow { get; set; }
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
uow = actionExecutedContext.Request.GetDependencyScope().GetService(typeof(IUnitOfWork)) as IUnitOfWork;
if (actionExecutedContext.Exception == null)
uow.Commit();
}
}
There is a lot more to the project but these are some of the core parts I wouldn't mind having an experienced eye look over.