MVC 3 - как реализовать сервисный уровень, нужны ли репозитории? - PullRequest
21 голосов
/ 14 сентября 2011

В настоящее время я создаю свое первое приложение MVC 3, используя EF Code First, SQL CE и Ninject. Я много читал об использовании репозиториев, единиц работы и уровней обслуживания. Я думаю, что я разобрался с основами, и я сделал свою собственную реализацию.

Это мои текущие настройки:

Сущность

public class Entity
{
    public DateTime CreatedDate { get; set; }
    public Entity()
    {
        CreatedDate = DateTime.Now;
    }
}

public class Profile : Entity
{
    [Key]
    public Guid UserId { get; set; }
    public string ProfileName { get; set; }

    public virtual ICollection<Photo> Photos { get; set; }

    public Profile()
    {
        Photos = new List<Photo>();
    }

public class Photo : Entity
{
    [Key]
    public int Id { get; set; }
    public Guid FileName { get; set; }
    public string Description { get; set; }

    public virtual Profile Profile { get; set; }
    public Photo()
    {
        FileName = Guid.NewGuid();
    }
}

SiteContext

public class SiteContext : DbContext
{
    public DbSet<Profile> Profiles { get; set; }
    public DbSet<Photo> Photos { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
    }
}

Интерфейс: IServices

public interface IServices : IDisposable
{
    PhotoService PhotoService { get; }
    ProfileService ProfileService { get; }

    void Save();
}

Реализация: Услуги

public class Services : IServices, IDisposable
{
    private SiteContext _context = new SiteContext();

    private PhotoService _photoService;
    private ProfileService _profileService;

    public PhotoService PhotoService
    {
        get
        {
            if (_photoService == null)
                _photoService = new PhotoService(_context);

            return _photoService;
        }
    }

    public ProfileService ProfileService
    {
        get
        {
            if (_profileService == null)
                _profileService = new ProfileService(_context);

            return _profileService;
        }
    }

    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);
    }
}

Интерфейс

public interface IPhotoService
{
    IQueryable<Photo> GetAll { get; }
    Photo GetById(int photoId);
    Guid AddPhoto(Guid profileId);
}

Осуществление

public class PhotoService : IPhotoService
{
    private SiteContext _siteContext;

    public PhotoService(SiteContext siteContext)
    {
        _siteContext = siteContext;
    }

    public IQueryable<Photo> GetAll
    {
        get
        {
            return _siteContext.Photos;
        }
    }

    public Photo GetById(int photoId)
    {
        return _siteContext.Photos.FirstOrDefault(p => p.Id == photoId);
    }

    public Guid AddPhoto(Guid profileId)
    {
        Photo photo = new Photo();

        Profile profile = _siteContext.Profiles.FirstOrDefault(p => p.UserId == profileId);

        photo.Profile = profile;
        _siteContext.Photos.Add(photo);

        return photo.FileName;
    }
}

Global.asax

protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        RegisterGlobalFilters(GlobalFilters.Filters);
        RegisterRoutes(RouteTable.Routes);

        ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());

        Database.SetInitializer<SiteContext>(new SiteInitializer());
    }

NinjectControllerFactory

public class NinjectControllerFactory : DefaultControllerFactory
{
    private IKernel ninjectKernel;
    public NinjectControllerFactory()
    {
        ninjectKernel = new StandardKernel();
        AddBindings();
    }
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        return controllerType == null
            ? null
        : (IController)ninjectKernel.Get(controllerType);
    }

    private void AddBindings()
    {
        ninjectKernel.Bind<IServices>().To<Services>();
    }
}

PhotoController

public class PhotoController : Controller
{
    private IServices _services;

    public PhotoController(IServices services)
    {
        _services = services;
    }

    public ActionResult Show(int photoId)
    {
        Photo photo = _services.PhotoService.GetById(photoId);

        if (photo != null)
        {
            string currentProfile = "Profile1";

            _services.PhotoService.AddHit(photo, currentProfile);

            _services.Save();

            return View(photo);
        }
        else
        {
            // Add error message to layout
            TempData["message"] = "Photo not found!";
            return RedirectToAction("List");
        }
    }

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

Я могу построить свое решение, и, похоже, оно работает правильно.

Мои вопросы:

  1. Есть ли какие-либо очевидные недостатки в моей реализации, которые я пропускаю?
  2. Смогу ли я использовать это с TDD? Обычно я вижу издевательства над репозиториями, но я не использовал это в приведенном выше, это вызовет проблемы?
  3. Правильно ли я использую DI (Ninject)?

Я программист-хобби, поэтому любые комментарии и / или предложения к моему коду приветствуются!

Ответы [ 3 ]

9 голосов
/ 14 сентября 2011

У вас есть общая идея, но для того, чтобы действительно привыкнуть к инъекции зависимости, требуется некоторое время.Я вижу ряд возможных улучшений:

  1. Ваш IServices интерфейс кажется ненужным.Я бы предпочел, чтобы контроллер определял, какие сервисы ему нужны (IPhotoService и т. Д.) Через его конструктор, а не через интерфейс IServices, как, например, какой-то строго типизированный локатор служб.
  2. Я виделDateTime.Now там?Как вы собираетесь проверить правильность установки даты в модульном тесте?Что если вы решите поддерживать несколько часовых поясов позже?Как насчет использования службы ввода даты для создания этого CreatedDate?
  3. Существует очень хорошее расширение Ninject специально для MVC.Он заботится о подключении к различным точкам, которые MVC 3 поддерживает для инъекций.Он реализует такие вещи, как ваш NinjectControllerFactory.Все, что вам нужно сделать, это заставить ваш класс Global расширить конкретное приложение на основе Ninject.
  4. Я бы предложил использовать NinjectModules для установки ваших привязок, а не устанавливать их в ControllerFactory.
  5. Рассмотрите возможность использования Binding by Convention, чтобы вам не приходилось явно привязывать каждый сервис к его реализации.

Обновление

Расширение Ninject MVC можно найти здесь .См. Раздел README для примера того, как расширить NinjectHttpApplication.В этом примере используются модули, о которых вы можете прочитать подробнее о здесь .(По сути, это просто место для размещения вашего кода привязки, чтобы вы не нарушали принцип единой ответственности.)

Что касается привязок на основе соглашений, общая идея состоит в том, чтобы ваш код привязки сканировал соответствующиесборки и автоматически связывать такие вещи, как IPhotoService с PhotoService в соответствии с соглашением об именах.Есть другое расширение здесь , чтобы помочь с такими вещами.С его помощью вы можете поместить такой код в ваш модуль:

Kernel.Scan(s =>
                {
                   s.From(assembly);
                   s.BindWithDefaultConventions();
                });

Приведенный выше код автоматически связывает каждый класс в данной сборке с любым интерфейсом, который он реализует в соответствии с соглашениями «по умолчанию» (например, Bind<IPhotoService>().To<PhotoService>()).

Обновление 2

Что касается использования одного и того же DbContext для всего запроса, вы можете сделать что-то вроде этого (используя библиотеку Ninject.Web.Common, которая требуется для расширения MVC):

Bind<SiteContext>().ToSelf().InRequestScope();

Тогда любые контекстно-зависимые сервисы, которые создает Ninject, будут использовать один и тот же экземпляр в запросе.Обратите внимание, что я лично использовал контексты с более коротким сроком действия, поэтому я не знаю, как изо всех сил вы бы заставили контекст располагаться в конце запроса, но я уверен, что это не будетслишком сложно.

4 голосов
/ 14 сентября 2011

Типы IServices и Services кажутся мне излишними.Если вы отбросите их и измените конструктор вашего контроллера на

public PhotoController(IPhotoService photoService, IProfileService profileService)
{
  _photoService = photoService;
  _profileService = profileService;
}

, будет более очевидно, от чего он на самом деле зависит.Более того, когда вы создаете новый контроллер, который действительно нуждается в IProfileService, вы можете просто передать IProfileService вместо полного IService, что дает новому контроллеру более легкую зависимость.

0 голосов
/ 28 июня 2013

Я могу утверждать, что ваши сервисы выглядят очень хорошо с репозиторием. Присмотритесь к интерфейсу:

IQueryable<Photo> GetAll { get; }
Photo GetById(int photoId);
Guid AddPhoto(Guid profileId);

Очень похоже на хранилище для меня. Возможно, потому что пример довольно прост, но я вижу смысл иметь сервис, если вы добавите в него логику прецедента. вместо этих довольно простых CRUD-операций.

И вы могли бы утверждать, что EF DbSet и DbContext являются репозиториями и единицей работы приложения ... и в этот момент мы входим в новую зону, которая несколько выходит за рамки вопроса.

...