6
\$\begingroup\$

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.

\$\endgroup\$
2
\$\begingroup\$

I do mostly Windows development so I might not be the experienced eye you're looking for in terms of ASP.NET MVC, but I'll share a couple thoughts anyway:

  • Your default/parameterless constructor for the AuthHandler class is redundant and can be removed - unless there's a parameterized constructor you've excluded from the posted code, the compiler generates the default constructor for you so putting it in is just clutter.
  • I'll have to look again at that specific chapter (the one specifically aimed at DI with ASP.NET MVC) in Mark Seeman's excellent Dependency Injection in .NET, but one thing I know for sure is that Mark Seeman wouldn't have gone with this IUnitOfWork stuff and I agree with the DI Guru - EF already implements Unit of Work and Repostory patterns, wrapping EF around your own is overkill.
    • Your generic repository is tied to EF anyway, since it's using DbSet<T>.
\$\endgroup\$
  • \$\begingroup\$ I'm not sure that you're correct with the last bit of your second bullet-point. Yes EF does coordinate changes, but it's about ensuring that all of your repositories all use a SINGLE EF DbContext. I can use EF, but if I have two instances of an EF DbContext then the changes won't be coordinated as intended with Unit Of Work \$\endgroup\$ – Luke Sep 13 '14 at 17:36
  • \$\begingroup\$ @Coulton All DbSets under a DbContext will use the same context. If you have more than a single context alive during a single transaction, I'd question the architecture. DbContext instances should be short-lived; the IoC container (here Ninject) should be configured to inject an instance per request into the controller or service that depends on it. Everything that happens during a request will use the same DbContext. \$\endgroup\$ – Mathieu Guindon Sep 13 '14 at 17:56
  • \$\begingroup\$ I think you probably know more about this that I do :). I agree that Ninject should be configured to create a single instance per request, but I thought that was the whole point in the unit of work if you choose to use it? \$\endgroup\$ – Luke Sep 13 '14 at 18:06
  • \$\begingroup\$ UoW is really about encapsulating a transaction; I find it useful when working with raw ADO.NET (heck, I even implemented one in VBA with ADODB).. but EF's DbContext does that all by itself. Managing object lifetime, in a DI setup, is the job of the IoC container ;) \$\endgroup\$ – Mathieu Guindon Sep 13 '14 at 18:14
  • \$\begingroup\$ Mat, I'd really appreciate it if you could take a look at this question of mine regarding this: stackoverflow.com/questions/25827376/… I don't know how I'm supposed to use a single DbContext for use by all of my repositories, which I assume I'm supposed to do? \$\endgroup\$ – Luke Sep 13 '14 at 20:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.