15
\$\begingroup\$

I've been studying C# for about 6 months and am trying to make a simple example for an n-tier application. I want to learn to do things in the most proper and professional way. This example uses a table in the database called "Settings" where various application settings can be persisted to the database. I figure once I can make a simple example, then I can have the knowledge to do something more advanced with the other tables in the database. I'm most concerned if this is a good simple example of a business logic layer.

There are 4 projects:

  1. AnimalDB.Models - POCO objects

  2. AnimalDB.DataAccess - Entity Framework DBContext, Repository, Unit of Work

  3. AnimalDB.Logic - Business Logic Layer

  4. AnimalDB.API - Web API

Here is my code for the project:

AnimalDB.Models

namespace AnimalDB.Models
{
    [Table("Settings")]
    public class Setting
    {
        [Key]
        public int SettingID { get; set; }

        [Required]
        [MaxLength(255)]
        public string Name { get; set; }

        [Required]
        [MaxLength(255)]
        public string Value { get; set; }
    }
}

AnimalDB.DataAccess

namespace AnimalDB.DataAccess
{
    public class AnimalDBContext: DbContext
    {
        public DbSet<Setting> Settings { get; set; }

        public AnimalDBContext():base("AnimalDB"){ }
    }
}

namespace AnimalDB.DataAccess
{
    public class GenericRepository<TEntity> where TEntity : class
    {
        internal AnimalDBContext context;
        internal DbSet<TEntity> dbSet;

        public GenericRepository(AnimalDBContext context)
        {
            this.context = context;
            this.dbSet = context.Set<TEntity>();
        }

        public virtual IEnumerable<TEntity> Get(
            Expression<Func<TEntity, bool>> filter = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            string includeProperties = "")
        {
            IQueryable<TEntity> query = dbSet;

            if (filter != null)
            {
                query = query.Where(filter);
            }

            foreach (var includeProperty in includeProperties.Split
                (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
            {
                query = query.Include(includeProperty);
            }

            if (orderBy != null)
            {
                return orderBy(query).ToList();
            }
            else
            {
                return query.ToList();
            }
        }

        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);
            Delete(entityToDelete);
        }

        public virtual void Delete(TEntity entityToDelete)
        {
            if (context.Entry(entityToDelete).State == EntityState.Detached)
            {
                dbSet.Attach(entityToDelete);
            }
            dbSet.Remove(entityToDelete);
        }

        public virtual void Update(TEntity entityToUpdate)
        {
            dbSet.Attach(entityToUpdate);
            context.Entry(entityToUpdate).State = EntityState.Modified;
        }
    }
}

namespace AnimalDB.DataAccess
{
    public class UnitOfWork : IDisposable
    {
        private AnimalDBContext context = new AnimalDBContext();
        private GenericRepository<Setting> settingRepository;

        public GenericRepository<Setting> SettingRepository
        {
            get
            {
                if (this.settingRepository == null)
                {
                    this.settingRepository = new GenericRepository<Setting>(context);
                }
                return settingRepository;
            }
        }

        public void Save()
        {
            context.SaveChanges();
        }

        private bool disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    context.Dispose();
                }
            }
            this.disposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}

AnimalDB.Logic

namespace AnimalDB.Logic
{
    public class SettingManager: IDisposable
    {
        private UnitOfWork unitOfWork;

        public SettingManager()
        {
            unitOfWork = new UnitOfWork();
        }

        public IQueryable<Setting> GetAll()
        {
            return unitOfWork.SettingRepository.Get(orderBy: q => q.OrderBy(d => d.Name)).AsQueryable();
        }

        public Setting Find(int? id)
        {
            return unitOfWork.SettingRepository.GetByID(id);
        }

        public void Insert(Setting setting)
        {
            unitOfWork.SettingRepository.Insert(setting);
            unitOfWork.Save();
        }

        public void Update(Setting setting)
        {
            unitOfWork.SettingRepository.Update(setting);
            unitOfWork.Save();
        }

        public void Delete(int? id)
        {
            Setting setting = unitOfWork.SettingRepository.GetByID(id);
            unitOfWork.SettingRepository.Delete(setting);
            unitOfWork.Save();
        }

        public void Dispose()
        {
            unitOfWork.Dispose();
        }
    }
}

AnimalDB.API

namespace AnimalDB.API.Models
{
    public class PostSettingViewModel
    {
        [Required]
        [MaxLength(255)]
        public string Name { get; set; }

        [Required]
        [MaxLength(255)]
        public string Value { get; set; }
    }

    public class PutSettingViewModel
    {
        [Required]
        public int SettingID { get; set; }

        [Required]
        [MaxLength(255)]
        public string Name { get; set; }

        [Required]
        [MaxLength(255)]
        public string Value { get; set; }
    }
}

namespace AnimalDB.API.Controllers
{
    public class SettingsController : ApiController
    {
        private SettingManager sm = new SettingManager();

        // GET api/Settings
        public IQueryable<Setting> GetSettings()
        {
            return sm.GetAll();
        }

        // GET api/Settings/5
        [ResponseType(typeof(Setting))]
        public IHttpActionResult GetSetting(int id)
        {
            Setting setting = sm.Find(id);
            if (setting == null)
            {
                return NotFound();
            }

            return Ok(setting);
        }

        // PUT api/Settings/5
        public IHttpActionResult PutSetting(int id, PutSettingViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (id != model.SettingID)
            {
                return BadRequest();
            }

            Setting setting = sm.Find(id);
            setting.Name = model.Name;
            setting.Value = model.Value;

            sm.Update(setting);

            return StatusCode(HttpStatusCode.NoContent);
        }

        // POST api/Settings
        [ResponseType(typeof(Setting))]
        public IHttpActionResult PostSetting(PostSettingViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            Setting setting = new Setting();
            setting.Name = model.Name;
            setting.Value = model.Value;

            sm.Insert(setting);

            return CreatedAtRoute("DefaultApi", new { id = setting.SettingID }, setting);
        }

        // DELETE api/Settings/5
        [ResponseType(typeof(Setting))]
        public IHttpActionResult DeleteSetting(int id)
        {
            Setting setting = sm.Find(id);
            if (setting == null)
            {
                return NotFound();
            }

            sm.Delete(id);

            return Ok(setting);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                sm.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

I like to try to make a simple example for myself so I can have an understanding and then build something more complex from there. Here are some things that bother me:

  1. I'm not using Interfaces and am not sure if they are necessary.
  2. I'm not sure where I would put error handling
  3. The business logic layer is very simple but that is only because its doing CRUD operations
  4. Why is the Unit Of Work and Repository necessary because I can just access entity framework in my business logic layer? I guess I'm not seeing the point.
  5. How could I use Dependency Inject and Inversion of Control? I'm not exactly sure what both are but I've read they should be used to make your application more testable.
\$\endgroup\$
4
  • 1
    \$\begingroup\$ So nobody has commented on this yet... I think it looks good from a quick glance. I haven't thoroughly went over it but from I've seen it looks good. Just thought I should say that since nobody has posted anything. \$\endgroup\$
    – Max
    Mar 6 '14 at 10:00
  • \$\begingroup\$ Excuse my ignorance, is the SettingManager here considered the Service? in many N-Layer implementations they use names like xxxService. Correct? \$\endgroup\$ Nov 3 '15 at 18:47
  • 1
    \$\begingroup\$ Yes, now that I have more experience, I would rename that as a service and use autofac for dependency injection. It's amazing what difference a year and a half of experience makes. \$\endgroup\$
    – David
    Nov 3 '15 at 18:52
  • \$\begingroup\$ @David you gave me hope, i am not a programmer at all.. just a hobby, i will be there in a few months hopefully :) \$\endgroup\$ Nov 3 '15 at 20:48
11
\$\begingroup\$

Of your questions, 1, 4, 5 all seem to hit on a common theme I think you might be missing the point of. The Single Responsibility Principle says that every object should have exactly one reason to change. The nuances of this become apparent if you were to try to write unit tests around your controller.

Without making any changes, it seems impossible to write tests for your SettingsController without hitting a real database. Go ahead and try it.

So how would you write tests for SettingsController without hitting a database? First thing you need to do is to remove the concern of persistence from the controller by inverting the control of it to something else. You are 90% of the way there already in that you have a class that handles this called the SettingsManager. What you need is an easy way of swapping out implementations of the SettingsManager without modifying the code of SettingsController. This is generally done by injecting an instance into the constructor of the SettingsController. But we aren't done yet. SettingsManager is a concrete class that cannot have its methods overridden. I would extract out an interface, maybe an ISettingsManager, and then instead of injecting an instance of SettingsManager to the SettingsController, you would instead inject an instance of ISettingsController.

So you are probably wondering how this helps. Now you could theoretically write a new implementation of ISettingsManager that stores settings in memory, or to DynamoDB, or MongoDB, or writes to a CSV on the filesystem, it doesn't matter. In testing you might even make a MockSettingsManager using something like Moq. Now what you have is a way of testing only the logic in the SettingsController separate from everything else.

As for validation, I sometimes struggle with that myself. I find it helpful to ask myself "Who should be responsible for validation?" or "Where would I put validation so that validation is that objects only responsibility?" or my personal favorite "Where would I put validation if I only was going to write tests for validation and nothing else?"

I strongly encourage you to do two things going forward that will make a lot of these concepts clearer.

  • Watch this talk from Uncle Bob called Architecture The Lost Years. Bob Martin gives some great background on where the SOLID principles come from and how they benefit your applications architecture as a whole.
  • Get in the habit of practicing Test Driven Development. It may feel painful and slow at first, but once you get in the habit of it you will find it does far more than reduce the bug count. It goes a long way to producing more maintainable code, code that follows better object-oriented patterns, and in the long run makes you a more productive programmer.
\$\endgroup\$
0

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.