Ответы пользователя по тегу Проектирование программного обеспечения
  • Как реализовать Asp.Net Identity 2.0 авторизацию на Onion-архитектуре?

    Valeriy1991
    @Valeriy1991
    Разработчик .NET C# (ASP.NET MVC) в Alfa-B, Moscow
    Добрый день!

    Я пока что обнаружил рабочий вариант, который очень хорошо тестируется.

    1. Создаем несколько проектов:
    - DAL.Abstract - лежат интерфейсы Generic-репозитория, специализированных репозиториев и UnitOfWork'а;
    - DAL.Real - содержит реализации интерфейсов из DAL.Abstract;
    - DAL.Fakes - fake-реализации для юнит-тестов;
    - BL.Abstract - лежат интерфейсы бизнес-сервисов;
    - BL.Real - здесь реализации интерфейсов бизнес-сервисов;
    - BL.Fakes - fake-реализации вспомогательных сервисов (EmailService, SmsService из Identity);
    - BL.Tests - юнит-тесты бизнес-логики;
    - Core.Models - модели предметной области (code-first).

    2. Т.к. я использую шаблон UnitOfWork, то пришлось отказаться от готовой реализации из библиотеки Identity.EntityFramework в силу того, что в ней расположены уже готовые классы User, Role, Claims и др., и приходится эту библиотеку тащить во все проекты, где нужно использовать хотя бы User'а. А вслед за ней во все проекты тащится библиотека EntityFramework... Мне это не нравится, потому что, на мой взгляд, слой MVC не должен знать про частности реализации (т.е. про EntityFramework).
    Поэтому самый простой вариант - сделать велоси... гм, собственную реализацию всяких там IUserStore, IPasswordStore и др.интерфейсов из Microsoft.AspNet.Identity.Core (ну и соответственно, создать собственные классы User, Role, UserRole, Claims, ExternalLogin), которые могли бы работать с нужным мне UnitOfWork'ом.

    3. Пишем тест:
    [TestFixture]
        public class UserServiceTests
        {
            private FakeEmailService _emaiService;
            private FakeSmsService _smsService;
            private IUserServiceFactory _userServiceFactory;
            private IUserService _userSrv;
            private IAuthenticationManager _authManager;
            private IUserRepository _userRepository;
    
            [SetUp]
            public void PreInit()
            {
                var authManagerMock = new Mock<IAuthenticationManager>();
                _authManager = authManagerMock.Object;
    
                _userRepository = new FakeUserRepository();
    
                _smsService = new FakeSmsService();
                _emaiService = new FakeEmailService();
                _userServiceFactory = new UserServiceFactory(
                    new FakeUnitOfWorkFactory(_userRepository),
                    _emaiService, _smsService, _authManager);
    
                _userSrv = _userServiceFactory.Create();
                // Заполняем хранилище пользователем "exist@exist.ru"
                RegisterExistUser();
            }
            private RegisterResult RegisterExistUser()
            {
                var model = new RegisterNewUserViewModel(ConstForTest.Users.ExistUser.Email, ConstForTest.Users.ExistUser.Password);
                return RegisterUser(model);
            }
            // Регистрация пользователя "valid@user.ru"
            private RegisterResult RegisterValidUser()
            {
                var model = new RegisterNewUserViewModel(ConstForTest.Users.ValidUser.Email, ConstForTest.Users.ValidUser.Password);
                return RegisterUser(model);
            }
    
            [Category(ConstForTest.Categories.UserServiceTests.RegisterNewUser)]
            [Test]
            public void RegisterNewUser_ValidEmailAndPassword_AfterRegistrationEmailDoesNotConfirmed()
            {
                var email = ConstForTest.Users.ValidUser.Email;
    
                // Act
                RegisterValidUser();
    
                var foundUser = _userSrv.GetUser(email);
                Assert.IsNotNull(foundUser);
                Assert.IsFalse(foundUser.EmailConfirmed);
            }
        }


    4. Пишем логику бизнес-сервисов. Приведу пример регистрации юзера:
    public class UserService : IUserService
        {
            private readonly IUnitOfWorkFactory _unitFactory;
            private readonly IUserIdentityManagerFactory _userManagerFactory;
            private readonly ISignInManagerFactory _signInManagerFactory;
    
            public UserService(IUnitOfWorkFactory unitFactory, 
                IUserIdentityManagerFactory userManagerFactory,
                ISignInManagerFactory signInManagerFactory)
            {
                _unitFactory = unitFactory;
                _userManagerFactory = userManagerFactory;
                _signInManagerFactory = signInManagerFactory;
            }
    
            public RegisterResult RegisterNewUser(RegisterNewUserViewModel model)
            {
                using (var unit = _unitFactory.Create())
                {
                    RegisterResult result = new RegisterResult();
                    IdentityResult identityResult;
                    try
                    {
                            var username = model.Email;
                            var manager = CreateUserManager(unit.UserRepository);
                            identityResult = manager.Create(new User(username), model.Password);
                            if (identityResult.Succeeded)
                            {
                                var createdUser = unit.UserRepository.GetByUsernameQuery(username);
                                manager.AddToRole(createdUser.Id, Roles.Client.ToString());
    
                                var confirmEmailCode = manager.GenerateEmailConfirmationToken(createdUser.Id);
                                result.ConfirmEmailCode = confirmEmailCode;
                                manager.SendEmail(createdUser.Id, Const.EmailSubjects.Account.ConfirmEmail,
                                    "<Тут содержимое  письма для подтверждения е-майл пользователя>");
                            }
                            unit.Commit();
                    }
                    catch (Exception ex)
                    {
                        identityResult = IdentityResult.Failed("В процессе регистрации возникла ошибка. Попробуйте выполнить операцию еще раз.");
                        unit.Rollback();
                    }
                    result.IdentityResult = identityResult;
                    return result;
                }
            }
            
            // ... другие методы бизнес-сервиса
            
        }


    Метод CreateUserManager(unit.UserRepository) создает экземпляр класса UserManager из библиотеки Microsoft.AspNet.Identity), передавая ему репозиторий работы с пользователями (да-да, т.к. у нас Identity, то необходимо вынести репозиторий для юзеров в отдельный интерфейс IUserRepository):
    private UserManager<User, Guid> CreateUserManager(IUserRepository userRepository)
            {
                return _userManagerFactory.Create(userRepository);
            }


    Фабрика создания UnitOfWork'а выглядит так:
    public interface IUnitOfWorkFactory
        {
            IUnitOfWork Create();
            IUnitOfWork Create(IsolationLevel level);
        }    
        public class EfUnitOfWorkFactory : IUnitOfWorkFactory
        {
            public IUnitOfWork Create()
            {
                return Create(IsolationLevel.ReadCommitted);
            }
    
            public IUnitOfWork Create(IsolationLevel level)
            {
                var uow = new EfUnitOfWork(level);
                return uow;
            }
        }

    Сам UnitOfWork:
    public interface IUnitOfWork : IDisposable, IUnitOfWorkRepositories
        {
            void SaveChanges();
            void Commit();
            void Rollback();
        }
        public interface IUnitOfWorkRepositories
        {
            IUserRepository UserRepository { get; }
        }
        public class EfUnitOfWork : IUnitOfWork
        {
            private bool _isDisposed = false;
            private DbContext _db;
            private DbContextTransaction _transaction;
    
            public EfUnitOfWork(IsolationLevel level = IsolationLevel.ReadCommitted)
            {
                _db = new MyApplicationDbContext();
                _transaction = _db.Database.BeginTransaction(level);
            }
    
            #region IDisposable
    
            protected virtual void Dispose(bool disposing)
            {
                if (!_isDisposed)
                {
                    if (disposing)
                    {
                        if (_transaction != null)
                        {
                            _transaction.Rollback();
                            _transaction.Dispose();
                            _transaction = null;
                        }
                        _db.Dispose();
                        _db = null;
                    }
                }
                _isDisposed = true;
            }
    
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
    
            #endregion
    
            #region IUnitOfWork
    
            public void SaveChanges()
            {
                _db.SaveChanges();
            }
    
            public void Commit()
            {
                SaveChanges();
                _transaction.Commit();
            }
    
            public void Rollback()
            {
                _transaction.Rollback();
            }
    
            #endregion
    
            private IUserRepository _userRepo;
    
            public IUserRepository UserRepository
            {
                get { return _userRepo ?? (_userRepo = new EfUserRepository(_db)); }
            }
        }


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

    P.S.
    1. Буду рад замечаниям со стороны опытных коллег относительно данной реализации.
    2. В качестве основы взяты материалы цикла статей
    3. Я придерживаюсь той концепции, что ни один Repository не должен знать про SaveChanges, а уж тем более про Commit и Rollback. Это прерогатива UnitOfWork'а, а Repository должен реализовывать лишь CRUD-операции. Поэтому метод SaveChanges вынесен в интерфейс IUnitOfWork.
    Ответ написан
    Комментировать
  • Как легче освоить внедрение зависимостей, code-first, TDD и паттерны?

    Valeriy1991
    @Valeriy1991
    Разработчик .NET C# (ASP.NET MVC) в Alfa-B, Moscow
    Добрый вечер! Спасибо за приглашение.
    На мой взгляд, Вам следует придерживаться следующих приоритетов по изучению:
    1. внедрение зависимостей более важно из Вашего списка, т.к. относится к SOLID принципам;
    2. TDD - на мой взгляд, вещь более нужная для изучения, чем code-first или patterns. Сам не так давно начал разрабатывать по TDD. Это очень здорово, что изобрели такой подход. Экономит тонну времени на ручное тестирование, а также дает быстрое понимание, что и где случайно (или неслучайно) сломалось. Главное - понимать, что покрывать тестами;
    3. третьим в список я бы добавил AngularJS или KnockoutJS или BackboneJS - сам пока что не изучил их и не начал применять, но судя по их популярности и преимуществам - думаю, Вам стоит с ними ознакомиться;
    4. code-first или database-first - не так уж и важно на Вашем этапе понимания. Главное отличие подхода Code-first: 1) модель пишется вручную, в связи с чем не нужно постоянно отслеживать edmx-диаграмму (т.е. можно считать, что Ваша code-first модель всегда находится в актуальном состоянии); 2) поддерживает миграции БД. В Database-first Вам нужно самому отслеживать актуальность состояния Вашей edmx-модели - с этим тоже могут быть проблемы. Опять же - только с опытом. С другой стороны, Database-first позволяет наглядно видеть Вашу модель, а вот Code-first - нет;
    5. паттернами тоже можете пока что голову не забивать. Безусловно, это нужно, но понимание их придет только с опытом (признаюсь честно: я сам не до конца все паттерны знаю и понимаю). На мой взгляд, важно соблюдать 1 основной паттерн - 3-уровневая архитектура (клиентский слой, слой бизнес-логики, слой работы с данными).

    Теперь по MVC.
    1. Model - тут, в принципе, всё просто: модель данных. Здесь можно поспорить, что иметь в виду под Моделью: модель самих данных или модель представления. Лично я после опыта работы с шаблоном MVVM в WPF под моделью данных в терминах ASP.NET MVC понимаю модель представления, а под термином "модель данных" - саму доменную модель (EF code-first, например). Кто-то может сказать, что это "лишняя работа" - по упаковыванию модели данных из EF-объектов в объекты модели представления. Да, частично соглашусь. Но зато это дает некую гарантию безопасности, что случайно пользователь не поменяет важную часть модели данных (например, ID).
    2. Контроллер. Основная задача контроллера - сформировать данные для отображения и передать эти данные в представление. Т.е. нужно стремиться к тому, чтобы код метода действия в контроллере содержал минимум кода. В идеале - вызов метода из слоя бизнес-логики и передача полученных данных в представление. Если Вы видите, что метод действия в контроллере содержит какую-то бизнес-логику, то это сигнал к рефакторингу: Ваш метод действия слишком много знает. По опыту могу добавить, что в среднем код метода действия содержит от 2 до 20-30 строк кода (с учетом того, что скобки { и } расположены на отдельных строках).
    3. Представление. Тут тоже всё просто: отобразить данные (из модели представления). Ни в коем случае нельзя в представлении писать логику по работе с самими данными, например, так делать нельзя:
    <div id="account">
        @{
            using(var db = new MyEfDbContext()
            {
                var userAccount = db.Accounts.FirstOrDefault(e => e.Username == User.Identity.Username);
                if(userAccount != null)
                {
                    @:Имя: @userAccount.Name
                    @:Фамилия: @userAccount.LastName
                }
            }
        }
    </div>


    Если у Вас 3-уровневая архитектура, например, есть слои:
    1. MyApp.MVC - MVC-application
    2. MyApp.BL - слой бизнес-логики
    3. MyApp.DAL - слой работы с данными
    то в представлении (View) вызывать напрямую сервисы бизнес-логики тоже нельзя, особенно, если Вы используете DI-принцип (внедрение зависимостей) и IoC контейнер. Т.е. такой пример недопустим:
    <div id="account">
        @{
            var accountService = new MyApp.BL.AccountService();
            var userAccount = accountService.GetUserAccountByUsername(User.Identity.Name);
            if(userAccount != null)
            {
                @:Имя: @userAccount.Name
                @:Фамилия: @userAccount.LastName
            }
        }
    </div>

    Попробую донести мысль архитектора и разработчика Александра Шевчука (преподавателя с http://itvdn.com): "Одна из главных целей при разработке - стремиться к упрощению системы". Ослабление зависимостей позволяет нам упрощать систему благодаря тому, что изменение осуществляется только на 1 каком-то слое/уровне. Если Вы во View вынесете логику по работе с данными, а уж тем более, как в примерах выше, работу с EF-контекстом, то Вы усилите зависимость одного компонента системы (MVC-слоя) от другого (слоя работы с данными или слоя бизнес-логики). Усиление зависимостей приводит к бОльшему числу изменений, что в свою очередь сказывается на повышении расходов на эту систему. Ослабление зависимостей приводит к меньшему числу изменений (например, при переходе от EF к native SQL или NHibernate затрагивается только слой работы с данными, а слой MVC и бизнес-логики не меняются), а значит, к более раннему выпуску системы или очередного релиза, и как следствие, снижение расходов (не только денежных, но и других ресурсов) на разработку. TDD тоже можно отнести к практикам, которые снижают затраты ресурсов на содержание системы. Но это я ушел в глобальное...

    Правильным будет подход, при котором у Вас снижается зависимость компонентов системы друг от друга, в случае с ASP.NET MVC приложением, на мой взгляд, это когда:
    - View знает о модели (я по прежнему буду иметь в виду модель представления - ViewModel, которые объявлены либо в MVC-слое, либо в слое бизнес-логики);
    - контроллер знает о слое бизнес-логики, обращается к нему за выполнением операций и получением ViewModel'ей, после чего передает во View полученную ViewModel (ну или JSON-данные);
    - Model формируется по принципу, грубо говоря, почти на каждую View своя модель.

    Фразу "правильным будет подход" я обосновываю тем, что у такого подхода есть масса плюсов (которые очевидны опытным разработчикам, но могут быть не до конца или неправильно поняты менее опытными коллегами, а именно):
    + View ничего не знает о доменной модели (только о ViewModel), благодаря чему Вы можете спокойно менять свою доменную модель, не внося изменений во View (см. выше про ослабление зависимостей и снижение количества связей). Также Вы спокойно можете перейти от EF к NHibernate или к native SQL, или использовать и то, и другое - View об этом никогда не узнает;
    + контроллер (да и весь MVC-слой) знает только о существовании слоя бизнес-логики, но ничего не знает о слое работы с данными.
    + если на View делать отдельную ViewModel, то это позволяет более полноценно управлять тем, что нужно показать пользователю. Т.е. дает возможность большего контроля отображаемых данных, повышает безопасность Вашего приложения (пользователь, например, не сможет изменить ID просматриваемой записи, если этого ID нет вообще в модели представления).

    Ну а вообще все зависит от задачи/проекта: нужно ли применять разбивку на слои или использовать ViewModel'и вместо обычных доменных моделей - надо думать над каждой ситуацией отдельно.

    P.S. На мой взгляд, литературу выбрали правильно - я тоже начинал изучение MVC с нее. Понравилась тем, что дается сначала общее описание и работа с ASP.NET MVC на сквозном примере. А потом идет более глубокое погружение в ASP.NET MVC. По разработке могу посоветовать блог Александра Бындю: blog.byndyu.ru Как мне кажется, там очень хорошо некоторые моменты разжевываются, в том числе SOLID, TDD, шаблон Repository, UnitOfWork и др.
    Ответ написан
    2 комментария
  • Правильное использование UnitOfWork в сервисах?

    Valeriy1991
    @Valeriy1991
    Разработчик .NET C# (ASP.NET MVC) в Alfa-B, Moscow
    Добрый день!

    По первому вопросу я бы отдал предпочтение using, потому как можно случайно забыть явно вызвать Dispose. А using, как известно, вызовет его сам.

    А вот по второму вопросу - боюсь, не смогу подсказать единственно верного решения. Я сам как-то столкнулся с такой проблемой, и долго думал, как ее решить. Особенно когда идет использование IoC-контейнеров. На одном из проектов компании, где сейчас работаю, используется такой подход: создается синглтон, у которого в качестве свойств перечислены интерфейсы, например:

    public sealed class Endpoint
        {
            ...
            // Закрытый конструктор
            private Endpoint()
            {
                Initialization();
            }
            ...
            
            // Реализация синглтона
            public static Endpoint Instance
            {
                get
                {
                    if (_instance == null)
                    {
                        lock (InstanceLocker)
                        {
                            if (_instance == null)
                                _instance = new Endpoint();
                        }
                    }
    
                    return _instance;
                }
            }
            ...
            public IMyService MyService { get; private set; }
            public ISomeService SomeService { get; private set; }
            ...


    И затем эта некая общая "точка доступа" используется следующим образом:

    var serviceResult = Endpoint.Instance.MyService.GetData();

    Т.к. это синглтон, то обращение Endpoint.Instance всегда создаст единственную реализацию вместе со всеми необходимыми сервисами.
    Из плюсов - доступ из любого места (в рамках разумного, конечно же) к любому сервису.
    Ответ написан
  • ASP.NET MVC DDD: зачем повторять один и тот же код 3 раза?

    Valeriy1991
    @Valeriy1991
    Разработчик .NET C# (ASP.NET MVC) в Alfa-B, Moscow
    Добрый день!

    Верным является такой подход, который решает поставленную задачу эффективно, быстро и просто. Моя деятельность связана с проектами по автоматизации бизнес-процессов заказчиков. В таких проектах лучше делить всю логику на несколько слоев: слой представлений (в частности, приложение на ASP.NET MVC, WPF, WinForms), слой бизнес-логики (отдельная библиотека, class library), слой доступа к данным (тоже отдельная библиотека).

    Опишу пример.
    В одном из своих личных проектов на ASP.NET используется как раз разделение логики на слои. Есть несколько проектов: MyApp.MVC, MyApp.DAL, MyApp.BL. Соответственно, общая схема работы такая: в контроллерах слоя MyApp.MVC идет обращение к методам из слоя MyApp.BL (слой бизнес-логики). Если методу из слоя MyApp.BL нужно поработать с данными БД, то он уже обращается к методам слоя работы с данными (MyApp.DAL). Слой MyApp.DAL уже непосредственно вытаскивает/добавляет/изменяет/удаляет данные. При этом слои не знают о конкретных реализациях методов, т.к. все базируется на интерфейсах и инверсии зависимостей (DI и IoC-контейнере, в частности - Ninject). В итоге что мы получаем:
    1. Разделение ответственности (каждый делает только то, что ему нужно, т.е. каждый выполняет свою задачу).
    2. Легкость сопровождения (нужно изменить логику выборки данных - например, изменить SQL-запрос. Все изменения коснутся только слоя MyApp.DAL, другие слои в этом случае менять не нужно и им будет "по барабану", что происходит внутри слоя MyApp.DAL).
    3. Расширяемость компонентов (здесь я имею в виду, что можно взять слой MyApp.DAL и включить его в другое приложение).
    4. Наглядность (код более чистый и последовательный).

    Я уверен, что это не "панацея", и в других типах проектах (например, игры? сложные математические модели?) такая архитектура может принести больше вреда, чем пользы (как пример, "продирание" через интерфейсы).
    Ответ написан
    Комментировать
  • Repository или CQRS?

    Valeriy1991
    @Valeriy1991
    Разработчик .NET C# (ASP.NET MVC) в Alfa-B, Moscow
    Добрый день!

    Честно говоря, не знаю, что именно Вам понравилось в CQRS и EventSourcing. На мой взгляд, это слишком сложный, непривычный подход, который может использоваться в исключительно редких случаях (как сказал Тимур Шемсединов, все зависит от задачи). Подходы к проектированию приложения нужно применять не тогда, когда они нравятся, а тогда, когда нужно решить конкретную проблему. Если при разработке приложений всегда делать "всякие такие интересные штуки", то процесс разработки уйдет в бесконечность... Проектируйте по принципу: есть задача - как лучше (под "лучше" я понимаю: просто, эффективно, быстро) ее решить. Всё. Если проект "исключительно" в целях обучения - то пожалуйста, здесь Вы вольны использовать всё, что душе угодно. Но если проект реальный, и он Вас "кормит", то тут следует сдерживать свое рвение к новым подходам и всегда задавать вопрос: эта технология/паттерн/механизм/подход поможет решить мне эту задачу? Да - значит, берем, учим, используем, внедряем. Нет - значит, нафиг. Потом больше геморроя будет с сопровождением и изменением кода (и думайте всегда о том разработчике, которому достанется Ваш код после Вас).
    Ответ написан
    Комментировать
  • Как организовать архитектуру приложений "Система управления проектами"?

    Valeriy1991
    @Valeriy1991
    Разработчик .NET C# (ASP.NET MVC) в Alfa-B, Moscow
    Добрый день!

    Хотелось бы добавить про контексты EF: stackoverflow.com/questions/9415955/c-sharp-workin...

    Поясню: когда я впервые столкнулся с масштабным применением EF, в том проекте использовался класс, в котором в виде поля объявлялся контекст. Т.е. контекст EF существовал все время, пока существовал этот класс (DataService). Так вот: все было хорошо до тех пор, пока не пришлось заняться ускорением работы приложения. И обнаружилось, что такой подход практически не позволяет использовать библиотеку TPL (Task Parallel Library), т.к. постоянно возникали ошибки, связанные с тем, что "контекст уже открыт". Ни в коем случае не используйте контекст EF по паттерну "Одиночка" и в статическом классе - наберетесь больших проблем и замучаетесь исправлять ошибки. Исходя из своего опыта (а также опыта моих коллег и друзей), пока что самым удачным применением EF является (как и написано в статье) использование контекста с коротким жизненным циклом, т.е. написали метод (который, например, получает список городов из БД и который, соответственно, будет внутри слоя доступа к данным), внутри инициализируем контекст (лучше с помощью using), получаем данные, оборачиваем их в модель (если надо), закрываем контекст и возвращаем данные наверх.

    P.S. Хотелось бы также порекомендовать Вам НЕ работать с классами, генерируемыми EF, в слое бизнес-логики, а писать для этих классов обертки (модели). В одном из проектов, которыми я занимался, используется WPF + MVVM + IoC (Unity Container), и в нем же весь интерфейс биндится к классам EF... Поверьте, это ужасно. Лучше сразу проектируйте так, чтобы классы EF у Вас использовались только внутри слоя доступа к данным, а наверху (в бизнес логике) вся работа ведется с классами-обертками над классами EF (т.е. чтобы классы слоя бизнес-логики ничего не знали о классах EF). В этом еще и такой плюс, что если Вы по каким-либо причинам решите отказаться от EF и перейти, например, на NHibernate или Native Sql, то все изменения коснутся только слоя доступа к данным.
    Ответ написан
    Комментировать