arame3333
arame3333

Reputation: 10193

Repository pattern with IoC - how should it be used properly?

I am a lone developer and Pluralsight is my salvation in helping me understand things like Repository and IoC, which I am just learning. However I am finding the pattern awkward to use.

So I am using Unity IoC, and following ModelContainer class;

public static class ModelContainer
{
    private static IUnityContainer instance;

    static ModelContainer()
    {
        instance = new UnityContainer();
    }

    public static IUnityContainer Instance
    {
        get
        {
            instance.RegisterType<ISCD_CallDiaryRepository, SCD_CallDiaryRepository>(new HierarchicalLifetimeManager());
            instance.RegisterType<ICompanyRepository, CompanyRepository>(new HierarchicalLifetimeManager());
            instance.RegisterType<ISubcontractorRepository, SubcontractorRepository>(new HierarchicalLifetimeManager());
            instance.RegisterType<ITradeRepository, TradeRepository>(new HierarchicalLifetimeManager());
            instance.RegisterType<IEmployeeRepository, EmployeeRepository>(new HierarchicalLifetimeManager());
            instance.RegisterType<ISubcontractorTradeRepository, SubcontractorTradeRepository>(new HierarchicalLifetimeManager());
            instance.RegisterType<ICountyRepository, CountyRepository>(new HierarchicalLifetimeManager());
            instance.RegisterType<IAddressLineRepository, AddressLineRepository>(new HierarchicalLifetimeManager());
            return instance;
        }
    }
}

allows me to instantiate the repository classes in the constructor of my controller;

public SubcontractorController(
        ISubcontractorRepository subcontractorRepository,
        ISubcontractorTradeRepository subcontractorTradeRepository, 
        ICompanyRepository companyRepository, 
        ISCD_CallDiaryRepository scdCallDiaryRepository,
        ITradeRepository tradeRepository,
        IAddressLineRepository addressLineRepository)
    {
        this.SubcontractorRepository = subcontractorRepository;
        this.SubcontractorTradeRepository = subcontractorTradeRepository;
        this.CompanyRepository = companyRepository;
        this.ScdCallDiaryRepository = scdCallDiaryRepository;
        this.TradeRepository = tradeRepository;
        this.AddressLineRepository = addressLineRepository;
    }

What I find awkward about this setup is that if I want to use the CRUD functionality in these repository classes, I have to either do the functionality in the controllers, or pass the repository instance I have created as a parameter into the model class methods. I choose the latter, controllers are not meant to do much, but it doesn't seem right to me to have to keep passing the repository parameter around.

What am I missing?

Upvotes: 3

Views: 4275

Answers (3)

M.Ob
M.Ob

Reputation: 1837

I break my MVC apps into several different projects.

  • AppName.Configuration: to handle any configuration of the app (i.e. pulling in web.config/app settings, etc)
  • AppName.Data: this is the data layer where all DB access is performed (no business logic). The DBML/EDMX lives here, my repository class(es) live here as well.
  • AppName.Models: this is where all of my ViewModels are defined for MVC, as well as other model objects needed throughout the application.
  • AppName.Services: This is my business layer, all everything must pass through here to get to the data layer or to the presentation layer. ViewModels are constructed from the database objects, data validation happens here, etc.
  • AppName.Web: this would be the MVC application. AppName.Data.Test: Unit tests for Data app
  • AppName.Services.Test: Unit tests for the services
  • AppName.Web.Test: Unit tests for the MVC controllers
  • AppName.Web.UI.Test: Unit tests for the web user interfaces (using WATIN)

I also have a set of classes packaged up into NuGet packages that I can add to my app if/when needed, namely (for this example):

  • CompanyName.Data: Common library for data layer logic
  • CompanyName.MVC: Common library for ASP.NET MVC integration
  • CompanyName.Utilities: Common library for miscellaneous utilities

My controllers do nothing except get the view models from the services layer to send to the views and then receive the data upon post from the views and send it off to the services layer for validation and saving back to the repository.

Here is a basic example:

This is the view model that will be used in this example:

public class CreateFocusViewModel
{
    public int CareerPlanningFormID { get; set; }

    public int PerformanceYear { get; set; }

    public IList<FocusModel> Focuses { get; set; }

    public string ResultsMeasuresFocusComments { get; set; }

    public byte MaximumFocusesAllowed { get; set; }
}

public class FocusModel
{
    public int FocusID { get; set; }

    public string FocusText { get; set; }

    public bool IsPendingDeletion { get; set; }
}

Sample Controller with GET and POST action methods:

public class CPFController : Controller
{
    private readonly ICareerPlanningFormService careerPlanningFormService;

    public CPFController(ICareerPlanningFormService careerPlanningFormService)
    {
        this.careerPlanningFormService = careerPlanningFormService;
    }

    [HttpGet]
    public ViewResult CreateFocus(int careerPlanningFormID)
    {
        var model = this.careerPlanningFormService.BuildCreateFocusViewModel(careerPlanningFormID);
        return this.View(model);
    }

    [HttpPost]
    public ActionResult CreateFocus(int careerPlanningFormID, string button)
    {
        var model = this.careerPlanningFormService.BuildCreateFocusViewModel(careerPlanningFormID);
        this.TryUpdateModel(model);

        switch (button)
        {
            case ButtonSubmitValues.Next:
            case ButtonSubmitValues.Save:
            case ButtonSubmitValues.SaveAndClose:
                {
                    if (this.ModelState.IsValid)
                    {
                        try
                        {
                            this.careerPlanningFormService.SaveFocusData(model);
                        }
                        catch (ModelStateException<CreateFocusViewModel> mse)
                        {
                            mse.ApplyTo(this.ModelState);
                        }
                    }

                    if (!this.ModelState.IsValid)
                    {
                        this.ShowErrorMessage(Resources.ErrorMsg_WEB_ValidationSummaryTitle);
                        return this.View(model);
                    }

                    break;
                }

            default:
                throw new InvalidOperationException(string.Format(Resources.ErrorMsg_WEB_InvalidButton, button));
        }

        switch (button)
        {
            case ButtonSubmitValues.Next:
                return this.RedirectToActionFor<CPFController>(c => c.SelectCompetencies(model.CareerPlanningFormID));

            case ButtonSubmitValues.Save:
                this.ShowSuccessMessage(Resources.Msg_WEB_NotifyBarSuccessGeneral);
                return this.RedirectToActionFor<CPFController>(c => c.CreateFocus(model.CareerPlanningFormID));

            case ButtonSubmitValues.SaveAndClose:
            default:
                return this.RedirectToActionFor<UtilityController>(c => c.CloseWindow());
        }
    }
}

Service layer where the ViewModel is built and data validated/saved:

public class CareerPlanningFormService : ICareerPlanningFormService
{
    private readonly IAppNameRepository repository;
    private readonly IPrincipal currentUser;

    public CareerPlanningFormService(IAppNameRepository repository, IPrincipal currentUser)
    {
        this.repository = repository;
        this.currentUser = currentUser;
    }

    public CreateFocusViewModel BuildCreateFocusViewModel(int careerPlanningFormID)
    {
        var cpf = this.repository.GetCareerPlanningFormByID(careerPlanningFormID);

        // create the model using cpf
        var model = new CreateFocusViewModel
        {
            CareerPlanningFormID = cpf.CareerPlanningFormID,
            PerformanceYear = cpf.PerformanceYearID,
            ResultsMeasuresFocusComments = cpf.ResultsMeasuresFocusComments,
            MaximumFocusesAllowed = cpf.PerformanceYear.MaximumCareerPlanningFormFocusesAllowed
            // etc., etc...
        };

        return model;
    }

    public void SaveFocusData(CreateFocusViewModel model)
    {
        // validate the model
        this.ValidateCreateFocusViewModel(model);

        // get the current state of the CPF
        var cpf = this.repository.GetCareerPlanningFormByID(model.CareerPlanningFormID);

        // bunch of code saving focus data here...

        // update the ResultsMeasuresFocusComments
        cpf.ResultsMeasuresFocusComments = string.IsNullOrWhiteSpace(model.ResultsMeasuresFocusComments) ? null : model.ResultsMeasuresFocusComments.Trim();

        // commit the changes
        this.repository.Commit();
    }

    private void ValidateCreateFocusViewModel(CreateFocusViewModel model)
    {
        var errors = new ModelStateException<CreateFocusViewModel>();

        {
            var focusesNotPendingDeletion = model.Focuses.Where(f => f.IsPendingDeletion == false);

            // verify that at least one of the focuses (not pending deletion) has a value
            {
                var validFocuses = focusesNotPendingDeletion.Where(f => !string.IsNullOrWhiteSpace(f.FocusText)).ToList();
                if (!validFocuses.Any())
                {
                    var index = model.Focuses.IndexOf(model.Focuses.Where(f => f.IsPendingDeletion == false).First());
                    errors.AddPropertyError(m => m.Focuses[index].FocusText, Resources.ErrorMsg_CPF_OneFocusRequired);
                }
            }

            // verify that each of the focuses (not pending deletion) length is <= 100
            {
                var focusesTooLong = focusesNotPendingDeletion.Where(f => f.FocusText != null && f.FocusText.Length > 100).ToList();
                if (focusesTooLong.Any())
                {
                    focusesTooLong.ToList().ForEach(f =>
                    {
                        var index = model.Focuses.IndexOf(f);
                        errors.AddPropertyError(m => m.Focuses[index].FocusText, Resources.ErrorMsg_CPF_FocusMaxLength);
                    });
                }
            }
        }

        errors.CheckAndThrow();
    }
}

Repository class:

public class AppNameRepository : QueryRepository, IAppNameRepository
{
    public AppNameRepository(IGenericRepository repository)
        : base(repository)
    {
    }

    public CareerPlanningForm GetCareerPlanningFormByID(int careerPlanningFormID)
    {
        return this.Repository.Get<CareerPlanningForm>().Where(cpf => cpf.CareerPlanningFormID == careerPlanningFormID).Single();
    }
}

Repository interface:

public interface IAppNameRepository : IRepository
{
    CareerPlanningForm GetCareerPlanningFormByID(int careerPlanningFormID);
}

Classes from the CompanyName.Data common library:

public abstract class QueryRepository : IRepository
{
    protected readonly IGenericRepository Repository;

    protected QueryRepository(IGenericRepository repository)
    {
        this.Repository = repository;
    }

    public void Remove<T>(T item) where T : class
    {
        this.Repository.Remove(item);
    }

    public void Add<T>(T item) where T : class
    {
        this.Repository.Add(item);
    }

    public void Commit()
    {
        this.Repository.Commit();
    }

    public void Refresh(object entity)
    {
        this.Repository.Refresh(entity);
    }
}

public interface IGenericRepository : IRepository
{
    IQueryable<T> Get<T>() where T : class;
}

public interface IRepository
{
    void Remove<T>(T item) where T : class;
    void Add<T>(T item) where T : class;
    void Commit();
    void Refresh(object entity);
}

I have both LinqToSQL and E.F., here is the setup for LinqToSQL:

internal sealed class LinqToSqlRepository : IGenericRepository
{
    private readonly DataContext dc;

    public LinqToSqlRepository(DataContext dc)
    {
        this.dc = dc;
    }

    public IQueryable<T> Get<T>() where T : class
    {
        return this.dc.GetTable<T>();
    }

    public void Remove<T>(T item) where T : class
    {
        this.dc.GetTable<T>().DeleteOnSubmit(item);
    }

    public void Add<T>(T item) where T : class
    {
        this.dc.GetTable<T>().InsertOnSubmit(item);
    }

    public void Commit()
    {
        this.dc.SubmitChanges();
    }

    public void Refresh(object entity)
    {
        this.dc.Refresh(RefreshMode.OverwriteCurrentValues, entity);
    }
}

This is in the CompanyName.Data common library as well. It is has methods to either register LinqToSQL or EntityFramework

public static class UnityContainerExtensions
{
    public static IUnityContainer RegisterEntityFrameworkClasses<TDbContext>(this IUnityContainer container, string nameOrConnectionString) where TDbContext : DbContext
    {
        var constructor = typeof(TDbContext).GetConstructor(new Type[] { typeof(string) });
        container.RegisterType<DbContext>(new HierarchicalLifetimeManager(), new InjectionFactory(c => constructor.Invoke(new object[] { nameOrConnectionString })));
        container.RegisterType<IGenericRepository, EntityFrameworkRepository>();
        return container;
    }

    public static IUnityContainer RegisterLinqToSqlClasses<TDataContext>(this IUnityContainer container, string connectionString) where TDataContext : DataContext
    {
        var constructor = typeof(TDataContext).GetConstructor(new Type[] { typeof(string) });
        container.RegisterType<DataContext>(new HierarchicalLifetimeManager(), new InjectionFactory(c => constructor.Invoke(new object[] { connectionString })));
        container.RegisterType<IGenericRepository, LinqToSqlRepository>();
        return container;
    }
}

In the CompanyName.Utilities library:

public interface IUnityBootstrap
{
    IUnityContainer Configure(IUnityContainer container);
}

Unity bootstrapping in AppName.Data

public class UnityBootstrap : IUnityBootstrap
{
    public IUnityContainer Configure(IUnityContainer container)
    {
        var config = container.Resolve<IAppNameConfiguration>();

        return container.RegisterLinqToSqlClasses<AppNameDataContext>(config.AppNameConnectionString)
                        .RegisterType<IAppNameRepository, AppNameRepository>();
    }
}

Unity bootstrapping in AppName.Services

public class UnityBootstrap : IUnityBootstrap
{
    public IUnityContainer Configure(IUnityContainer container)
    {
        new CompanyName.Security.UnityBootstrap().Configure(container);
        new AppName.Data.UnityBootstrap().Configure(container);

        container.RegisterSecureServices<AuthorizationRulesEngine>(typeof(UnityBootstrap).Assembly);

        return container.RegisterType<ICareerPlanningFormService, CareerPlanningFormService>()
                        .RegisterType<IStaffService, StaffService>();
    }
}

Unity bootstrapping in AppName.Web

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        // Standard MVC setup
        AreaRegistration.RegisterAllAreas();

        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);

        // Application configuration 
        var container = new UnityContainer();
        new CompanyName.Mvc.UnityBootstrap().Configure(container);
        new AppName.Configuration.UnityBootstrap().Configure(container);
        new AppName.Data.UnityBootstrap().Configure(container);
        new AppName.Services.UnityBootstrap().Configure(container);

        // Default MVC model binder is pretty weak with collections
        ModelBinders.Binders.DefaultBinder = new DefaultGraphModelBinder();
    }

    protected void Application_Error()
    {
        HttpApplicationEventHandler.OnError(this.Context);
    }

    protected void Application_EndRequest()
    {
        HttpApplicationEventHandler.OnEndRequest(this.Context);
    }
}

Upvotes: 5

Akim
Akim

Reputation: 8669

In provided sample, there is SubcontractorController controller with six dependecies, what is usually means that the controller tries to do too much at once, and could possibly violates Single responsibility principle. There are at least three way to improve situation:

  • Instead of injecting repositories into a controller, and passing the repository instance as a parameter into the model class methods, you could refactor to Aggregate Services, i.e. combine both models method and dependencies in single entity. This will also solve constructor over-injection.
  • Inject a factory of repositories into the controller, and later resolve dependencies using this factory
  • Inject models themselves, and make repositories to be their dependencies.

Upvotes: 5

Kye
Kye

Reputation: 6239

I tried the latter as well early on in my career. It didn't end well. You end up with unexpected performance hits, and large model objects. I think the controller is the perfect place for the logic you mention. If your worried about ui and business concerns in the same class, move the business out to a service class.

Upvotes: 1

Related Questions