Руководство по структурированию вашего проекта в виде нескольких более мелких модульных частей.

Задний план

Недавно я сделал пост, подробно описывающий, как настроить сервер .NET Core с базой данных. Теперь я хочу расширить этот пост (как и позже в других аспектах), чтобы поговорить о способах улучшения этого проекта, а именно о философии луковой архитектуры.

Цель этого поста — рассказать, как я обновил свой существующий проект, чтобы сделать его более разрозненным, а также объяснить, почему я принял те решения, которые принял. Надеюсь, к концу я убедил вас, что структура этого проекта явно лучше.

Проблема

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

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

Разделение проекта

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

Обратите внимание, что наши репозитории, контроллеры, базы данных и модели представлений собраны вместе. Цель этого поста — перейти к структуре, в которой наши модели представлений не зависят от наших моделей баз данных, наши контроллеры не зависят от конкретных репозиториев и т. д.

Сначала мы создадим два новых проекта с именами UI и Integration.EntityFramework и инициализируем новый проект в каждом из них. Самой утомительной частью этого процесса будет убедиться, что все новые пространства имен совпадают (хотя, если вы используете настоящую IDE, такую ​​как Visual Studio, это должно быть гораздо меньшей проблемой), поэтому обязательно потратьте дополнительное время на исправление этих файл за файлом — я буду опускать эту часть руководства, надеясь, что вы позаботитесь об этом по ходу дела. И, как всегда, заранее приносим извинения за любое форматирование кода, которое превращается в странную многострочную неразбериху.

Теперь, когда у нас есть новые проекты, мы также должны добавить файл global.json, в котором перечислены все проекты, найденные в корневом каталоге.

{
    "projects":[
        "Core",
        "Integration.EntityFramework",
        "Test.Unit",
        "UI"
    ]
}

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

Проект EntityFramework будет содержать такие вещи, как наши репозитории и модели баз данных.

Хотя проект Core потеряет большую часть своего содержимого, он также получит некоторые новые элементы, а именно все интерфейсы и модели предметной области нашего проекта. К тому времени, когда мы закончим, эти новые вещи станут сердцем (или ядром) нашего приложения, а новые проекты будут как можно меньше общаться друг с другом.

Основной проект

Поскольку проект Core является основой нашего приложения, имеет смысл начать с него. Добавьте UserDomainModel.cs в папку Models.

namespace Core.Models
{
    public class UserDomainModel
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string Password { get; set; }
    }
}

Вопрос о том, необходимы ли все эти поля, спорный, но их наличие, а затем отсутствие необходимости в них почти наверняка устранит больше головной боли, чем вызовет. Помните, что модель предметной области не будет обращена к пользователю, поэтому у нас все еще есть возможность ограничить данные, которые мы раскрываем при преобразовании в модель представления позже. Затем создайте папку Interfaces и добавьте в нее IUserRepository.cs — это будет шаблон для нашего UserRepository.

using Core.Models;
namespace Core.Interfaces
{
    public interface IUserRepository
    {
        UserDomainModel GetByEmail(string email);
        void Save(UserDomainModel userToSave);
    }
}

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

Проект Entity Framework

Теперь у нас есть база для других наших проектов. Далее приступим к заполнению проекта EntityFramework. Наш проект представляет собой project.json, который выглядит следующим образом.

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {
    "Microsoft.EntityFrameworkCore": "1.0.0"
  },
  "frameworks": {
    "netcoreapp1.0": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.0.0"
        },
        "Core": {
          "target": "project"
        }
      },
      "imports": "dnxcore50"
    }
  },
  "tools": {
    "Microsoft.EntityFrameworkCore.Tools": {
      "version": "1.0.0-preview2-final",
      "imports": "portable-net45+win8+dnxcore50"
    }
  }
}

Поскольку это проект EntityFramework, логично, что основными зависимостями являются Entity Framework. Обратите внимание на раздел frameworks и список зависимостей. В новой записи мы указываем этому проекту ссылаться на проект Core.

Создайте новую папку с именем Models и скопируйте сюда файлы UserDatabaseModel.cs и DatabaseContext.cs из Core/Models. Прежде чем мы начнем работать с репозиториями, давайте создадим некоторую инфраструктуру — нам понадобятся некоторые преобразователи для преобразования в модели базы данных и предметной области и из них.

Создайте папку Mappers и добавьте UserDomainModelMapper.cs, чтобы перейти от модели базы данных к модели домена.

using Core.Models;
using Integration.EntityFramework.Models;
namespace Integration.EntityFramework.Mappers
{
    public static class UserDomainModelMapper
    {
        public static UserDomainModel MapFrom(UserDatabaseModel databaseModel)
        {
            return new UserDomainModel
            {
                Id = databaseModel.Id,
                FirstName = databaseModel.FirstName,
                LastName = databaseModel.LastName,
                Email = databaseModel.Email,
                Password = databaseModel.Password
            };
        }
    }
}

Затем создайте UserDatabaseModelMapper.cs, чтобы перейти от модели предметной области к модели базы данных.

using Core.Models;
using Integration.EntityFramework.Models;
namespace Integration.EntityFramework.Mappers
{
    public static class UserDatabaseModelMapper
    {
        public static UserDatabaseModel MapFrom(UserDomainModel domainModel)
        {
            return new UserDatabaseModel
            {   
                Id = domainModel.Id,
                FirstName = domainModel.FirstName,
                LastName = domainModel.LastName,
                Email = domainModel.Email,
                Password = domainModel.Password
            };
        }
    }
}

Эти преобразователи могут показаться избыточными, потому что в этом случае поля в обеих моделях совпадают, но представьте случай, когда модель базы данных имеет поля, допускающие значение NULL. Путем сопоставления с моделью предметной области можно использовать пустые строки или нули или любое другое соглашение, которое вы хотите использовать вместо необходимости иметь дело с нулевыми значениями (поскольку нулевые значения раздражают, а с конкретными типами почти всегда легче работать).

Теперь, когда у нас есть это, мы можем скопировать папку Repositories из Core и начать исправлять UserRepository. Основная проблема заключается в том, чтобы убедиться, что IUserRepository правильно реализован — наши новые преобразователи будут очень полезны в этом.

using System.Linq;
using Core.Interfaces;
using Core.Models;
using Integration.EntityFramework.Mappers;
using Integration.EntityFramework.Models;
namespace Integration.EntityFramework.Repositories
{
    public class UserRepository : IUserRepository
    {
        private readonly DatabaseContext _databaseContext;
public UserRepository(DatabaseContext databaseContext)
        {
            _databaseContext = databaseContext;
        }
public UserDomainModel GetByEmail(string email)
        {
            var userFromDb = _databaseContext.Users.SingleOrDefault(x => x.Email == email);
            return UserDomainModelMapper.MapFrom(userFromDb);
        }
public void Save(UserDomainModel userToSave)
        {
            var mappedDbModel = UserDatabaseModelMapper.MapFrom(userToSave);
            var userFromDb = _databaseContext.Users.SingleOrDefault(x => x.Id == mappedDbModel.Id);
if(userFromDb != null)
            {
                userFromDb.FirstName = mappedDbModel.FirstName;
                userFromDb.LastName = mappedDbModel.LastName;
                userFromDb.Email = mappedDbModel.Email;
                userFromDb.Password = mappedDbModel.Password;
            }
            else
            {
                _databaseContext.Users.Add(mappedDbModel);
            }
            _databaseContext.SaveChanges();
        }
    }
}

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

Аналогичным образом в методе Сохранить мы теперь берем модель предметной области и сопоставляем ее с моделью базы данных, а затем выполняем нашу логику с этой новой сопоставленной моделью. Разочаровывает то, как меняется этот метод, что мы должны установить каждое поле в отслеживаемом объекте базы данных, чтобы Entity Framework знала, что оно было обновлено.

Возможно, я упоминал об этом раньше, но в предыдущих версиях Entity Framework была функция AddOrUpdate, которая делала именно то, что выполняет метод Save, но ее еще нужно было реализовать в Entity. Framework Core (который, как я узнал, на самом деле является полностью переписанным для .NET Core) на момент написания этой статьи.

На этом проект EntityFramework завершен, и мы можем перейти к исправлению проекта UI. Пока не пытайтесь строить, потому что наше приложение ужасно искалечено и будет кричать от боли.

Проект пользовательского интерфейса

Как и в случае с проектом EntityFramework, давайте начнем с того, как должен выглядеть новый файл project.json для этого проекта.

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },
  "dependencies": {
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
    "Microsoft.AspNetCore.Mvc": "1.0.0",
    "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0-*",
    "Microsoft.Extensions.Configuration.Json": "1.0.0-rc2-final",
    "Microsoft.EntityFrameworkCore": "1.0.0",
    "Npgsql.EntityFrameworkCore.PostgreSQL": "1.0.0"
  },
  "frameworks": {
    "netcoreapp1.0": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.0.0"
        },
        "Core": {
          "target": "project"
        },
        "Integration.EntityFramework": {
          "target": "project"
        }
      },
      "imports": "dnxcore50"
    }
  },
  "tools": {
    "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final"
  },
  "publishOptions": {
    "include": [
      "Areas",
      "Views",
      "wwwroot",
      "config.json",
      "web.config"
    ]
  }
}

Здесь обратите внимание, что у нас есть зависимости от проектов Core и EntityFramework. Я знаю, что сказал, что мы хотим, чтобы эти проекты касались только проекта Core, но я хотел избежать решений, которые казались бы волшебством, пока я более полно не понял, что именно они делают, особенно для внедрение зависимостей (мы вернемся к этому ближе к концу этого раздела).

Как вы вскоре увидите, проект UI на самом деле зависит от проекта EntityFramework только в нескольких избранных местах, которые касаются почти исключительно настройки нашего сервера. Усилия по обновлению этих файлов в случае, если мы решим, что Entity Framework ужасен, и мы захотим использовать какую-то другую технологию, довольно невелики, поэтому я не слишком беспокоюсь об этом в настоящее время.

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

Насколько я понимаю, это как-то связано со всеми проектами, рассматриваемыми как консольные приложения в .NET Core. Я хочу найти некоторые обходные пути или решения, потому что файлы Program.cs кажутся довольно бесполезными в проектах Core и EntityFramework.

На данный момент у нас должен быть UI Program.cs, который выглядит следующим образом.

using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
namespace UI
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseKestrel()
                .UseStartup<Startup>()
                .Build();
            host.Run();
        }
    }
}

И Core Program.cs, в котором почти ничего нет.

using System;
namespace Core
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //required entry point
        }
    }
}

Это раздражает, но я принял это и пошел дальше. Если и когда я выясню, как исправить это, я обязательно напишу об этом.

Поскольку appsettings.json — очень простое обновление, давайте разберемся с этим сейчас. Переместите его из Основного. Готово — теперь Startup.cs.

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Integration.EntityFramework.Models;
namespace UI
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
var connectionString = Configuration["DbContextSettings:ConnectionString"];
            services.AddDbContext<DatabaseContext>(opts => opts.UseNpgsql(connectionString));
        }
public void Configure(IApplicationBuilder app)
        {
            app.UseMvc();
        }
    }
}

Моя проблема с этим файлом заключается в том, насколько громоздким он станет, когда проект станет достаточно сложным. Есть несколько способов исправить это — я решил создать вспомогательный класс, который, надеюсь, поможет сделать код более читабельным сейчас и в будущем.

Создайте папку Helpers и создайте файл StartupHelper.cs.

using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Integration.EntityFramework.Models;
namespace UI.Helpers
{
    public static class StartupHelper
    {
        public static void AddDatabaseConnectionToServices(IServiceCollection services, IConfigurationRoot Configuration)
        {
            var connectionString = Configuration["DbContextSettings:ConnectionString"];
            services.AddDbContext<DatabaseContext>(opts => opts.UseNpgsql(connectionString));
        }
        public static IConfigurationBuilder GetConfigurationBuilder(IHostingEnvironment env)
        {
            return new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables();
        }
    }
}

Моя цель состоит в том, чтобы каждый раз, когда я захочу что-то добавить к службам в будущем, я смогу создать небольшой метод AddWhateverToServices' , который нужно будет вызывать только при запуске. .cs в одном месте. Независимо от того, как изменится этот помощник, Startup.cs теперь становится намного более читабельным и понятным в отношении его назначения.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using UI.Helpers;
namespace UI
{
    public class Startup
    {
        public IConfigurationRoot Configuration { get; }
        public Startup(IHostingEnvironment env)
        {
            var builder = StartupHelper.GetConfigurationBuilder(env);
            Configuration = builder.Build();
        }
        public void Configure(IApplicationBuilder app)
        {
            app.UseMvc();
        }
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            StartupHelper.AddDatabaseConnectionToServices(services, Configuration);
        }
    }
}

Обратите внимание, что Startup больше не зависит от Entity Framework и зависит только от нашего проекта Core! Теперь, если мы заменим Entity Framework, нам нужно будет коснуться только одного файла в этом проекте — StartupHelper.

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

Нам понадобится новая папка Models для UserViewModel, которую в настоящее время можно найти в Core/Models/View.

namespace UI.Models
{
    public class UserViewModel
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
    }
}

И новая папка Mappers для UserViewModelMapper, расположенная в Core/Mappers. Мы добавим несколько обновлений в карту из UserDomainModel вместо UserDatabaseModel, которую мы использовали ранее.

using Core.Models;
using UI.Models;
namespace UI.Mappers
{
    public static class UserViewModelMapper
    {
        public static UserViewModel MapFrom(UserDomainModel domainModel)
        {
            return new UserViewModel
            {
                FirstName = domainModel.FirstName,
                LastName = domainModel.LastName,
                Email = domainModel.Email
            };
        }
    }
}

Почти готово! Чтобы завершить проект UI, нам еще нужно разобраться с контроллерами и их представлениями. Переместите HomeController из Core/Controllers в новую папку Controllers, а соответствующие представления из Core/Views/Home в идентичную структуру в этой папке. проект — просто не забудьте обновить Index.cshtml, чтобы он использовал правильную модель.

using System;
using Microsoft.AspNetCore.Mvc;
using Core.Interfaces;
using UI.Mappers;
namespace UI.Controllers
{
    public class HomeController : Controller
    {
        private readonly IUserRepository _userRepository;
        public HomeController(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }
        [HttpGet("/")]
        public ActionResult Index()
        {
            var email = "[email protected]";
            var user = _userRepository.GetByEmail(email);
            var userViewModel = UserViewModelMapper.MapFrom(user);
            return View(userViewModel);
        }
    }
}

Обратите внимание, что мы внедряем IUserRepository вместо использования DatabaseContext — мы больше не связаны с проектом EntityFramework и полагаемся исключительно на Core для определения того, что сможет делать наш UserRepository.

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

Последний шаг перед тем, как мы сможем получить проект и снова запустить его, — это зарегистрировать наши недавно введенные зависимости — нам нужно сообщить нашему приложению, что, черт возьми, делать с этим IUserRepository, когда он внедряется в контроллер. Нам нужно будет добавить несколько небольших обновлений в StartupHelper.cs.

using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Core.Interfaces;
using Integration.EntityFramework.Models;
using Integration.EntityFramework.Repositories;
namespace UI.Helpers
{
    public static class StartupHelper
    {
        ...
        public static void AddDependencyInjectionToServices(IServiceCollection services)
        {
            services.AddScoped<IUserRepository, UserRepository>();
        }
    }
}

Мы указываем приложению использовать UserRepository везде, где мы внедрили IUserRepository. AddScoped говорит приложению создавать только один из этих UserRepositories при получении запроса, а затем удерживать его до тех пор, пока все его операции не будут выполнены — если другой запрос позже, будет создан новый UserRepository.

Чтобы запустить эту работу, нам нужно обновить Startup.cs.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using UI.Helpers;
namespace UI
{
    public class Startup
    {
        ...
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            StartupHelper.AddDatabaseConnectionToServices(services, Configuration);
            StartupHelper.AddDependencyInjectionToServices(services);
        }
    }
}

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

Резюме

Мы внесли много изменений во все эти проекты, поэтому я собираюсь пройтись по каждому из них, чтобы написать, что у меня получилось в моем финальном проекте. Сначала давайте сделаем один файл, которого нет ни в одном из небольших проектов.

global.json

{
    "projects":[
        "Core",
        "Integration.EntityFramework",
        "Test.Unit",
        "UI"
    ]
}

А теперь по каждому из отдельных подпроектов.

Основной проект

Изображение того, как в итоге выглядел проект…

И код для каждого соответствующего файла…

IUserRepository.cs

using Core.Models;
namespace Core.Interfaces
{
    public interface IUserRepository
    {
        UserDomainModel GetByEmail(string email);
        void Save(UserDomainModel userToSave);
    }
}

UserDomainModel.cs

namespace Core.Models
{
    public class UserDomainModel
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string Password { get; set; }
    }
}

Program.cs

using System;
namespace Core
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //required entry point
        }
    }
}

project.json

{
  "version": "2.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {},
  "frameworks": {
    "netcoreapp1.0": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.0.0"
        }
      },
      "imports": "dnxcore50"
    }
  },
  "tools": {}
}

Проект Integration.EntityFramework

Изображение того, как в итоге выглядел проект…

И код для каждого соответствующего файла…

UserDatabaseModelMapper.cs

using Core.Models;
using Integration.EntityFramework.Models;
namespace Integration.EntityFramework.Mappers
{
    public static class UserDatabaseModelMapper
    {
        public static UserDatabaseModel MapFrom(UserDomainModel domainModel)
        {
            return new UserDatabaseModel
            {   
                Id = domainModel.Id,
                FirstName = domainModel.FirstName,
                LastName = domainModel.LastName,
                Email = domainModel.Email,
                Password = domainModel.Password
            };
        }
    }
}

UserDomainModelMapper.cs

using Core.Models;
using Integration.EntityFramework.Models;
namespace Integration.EntityFramework.Mappers
{
    public static class UserDomainModelMapper
    {
        public static UserDomainModel MapFrom(UserDatabaseModel databaseModel)
        {
            return new UserDomainModel
            {
                Id = databaseModel.Id,
                FirstName = databaseModel.FirstName,
                LastName = databaseModel.LastName,
                Email = databaseModel.Email,
                Password = databaseModel.Password
            };
        }
    }
}

DatabaseContext.cs

using Microsoft.EntityFrameworkCore;
namespace Integration.EntityFramework.Models
{
    public class DatabaseContext : DbContext
    {
        public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) { }
public DbSet<UserDatabaseModel> Users{ get; set; }
    }
}

UserDatabaseModel.cs

using System.ComponentModel.DataAnnotations.Schema;
namespace Integration.EntityFramework.Models
{
    [Table("user")]
    public class UserDatabaseModel
    {
        [Column("id")]
        public int Id { get; set; }
        [Column("firstname")]
        public string FirstName { get; set; }
        [Column("lastname")]
        public string LastName { get; set; }
        [Column("email")]
        public string Email { get; set; }
        [Column("password")]
        public string Password { get; set; }
    }
}

UserRepository.cs

using System.Linq;
using Core.Interfaces;
using Core.Models;
using Integration.EntityFramework.Mappers;
using Integration.EntityFramework.Models;
namespace Integration.EntityFramework.Repositories
{
    public class UserRepository : IUserRepository
    {
        private readonly DatabaseContext _databaseContext;
public UserRepository(DatabaseContext databaseContext)
        {
            _databaseContext = databaseContext;
        }
public UserDomainModel GetByEmail(string email)
        {
            var userFromDb = _databaseContext.Users.SingleOrDefault(x => x.Email == email);
            return UserDomainModelMapper.MapFrom(userFromDb);
        }
public void Save(UserDomainModel userToSave)
        {
            var mappedDbModel = UserDatabaseModelMapper.MapFrom(userToSave);
            var userFromDb = _databaseContext.Users.SingleOrDefault(x => x.Id == mappedDbModel.Id);
if(userFromDb != null)
            {
                userFromDb.FirstName = mappedDbModel.FirstName;
                userFromDb.LastName = mappedDbModel.LastName;
                userFromDb.Email = mappedDbModel.Email;
                userFromDb.Password = mappedDbModel.Password;
            }
            else
            {
                _databaseContext.Users.Add(mappedDbModel);
            }
            _databaseContext.SaveChanges();
        }
    }
}

Program.cs

using System;
namespace Integration.EntityFramework
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //required entry point
        }
    }
}

project.json

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {
    "Microsoft.EntityFrameworkCore": "1.0.0"
  },
  "frameworks": {
    "netcoreapp1.0": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.0.0"
        },
        "Core": {
          "target": "project"
        }
      },
      "imports": "dnxcore50"
    }
  },
  "tools": {
    "Microsoft.EntityFrameworkCore.Tools": {
      "version": "1.0.0-preview2-final",
      "imports": "portable-net45+win8+dnxcore50"
    }
  }
}

Пользовательский интерфейс проекта

Изображение того, как в итоге выглядел проект…

И код для каждого соответствующего файла…

HomeController.cs

using System;
using Microsoft.AspNetCore.Mvc;
using Core.Interfaces;
using UI.Mappers;
namespace UI.Controllers
{
    public class HomeController : Controller
    {
        private readonly IUserRepository _userRepository;
public HomeController(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }
[HttpGet("/")]
        public ActionResult Index()
        {
            var email = "[email protected]";
            var user = _userRepository.GetByEmail(email);
var userViewModel = UserViewModelMapper.MapFrom(user);
            return View(userViewModel);
        }
    }
}

StartupHelper.cs

using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Core.Interfaces;
using Integration.EntityFramework.Models;
using Integration.EntityFramework.Repositories;
namespace UI.Helpers
{
    public static class StartupHelper
    {
        public static IConfigurationBuilder GetConfigurationBuilder(IHostingEnvironment env)
        {
            return new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables();
        }
public static void AddDatabaseConnectionToServices(IServiceCollection services, IConfigurationRoot Configuration)
        {
            var connectionString = Configuration["DbContextSettings:ConnectionString"];
            services.AddDbContext<DatabaseContext>(opts => opts.UseNpgsql(connectionString));
        }
public static void AddDependencyInjectionToServices(IServiceCollection services)
        {
            services.AddScoped<IUserRepository, UserRepository>();
        }
    }
}

UserViewModelMapper.cs

using Core.Models;
using UI.Models;
namespace UI.Mappers
{
    public static class UserViewModelMapper
    {
        public static UserViewModel MapFrom(UserDomainModel domainModel)
        {
            return new UserViewModel
            {
                FirstName = domainModel.FirstName,
                LastName = domainModel.LastName,
                Email = domainModel.Email
            };
        }
    }
}

UserViewModel.cs

namespace UI.Models
{
    public class UserViewModel
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
    }
}

Index.cshtml

@model UI.Models.UserViewModel
<div>@Model.FirstName @Model.LastName</div>
<div>@Model.Email</div>

appsettings.json

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "DbContextSettings" :{
    "ConnectionString" : "User ID=testuser;Password=test;Host=localhost;Port=5432;Database=TestDatabase;Pooling=true;"
  }
}

Program.cs

using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
namespace UI
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseKestrel()
                .UseStartup<Startup>()
                .Build();
            host.Run();
        }
    }
}

project.json

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },
  "dependencies": {
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
    "Microsoft.AspNetCore.Mvc": "1.0.0",
    "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0-*",
    "Microsoft.Extensions.Configuration.Json": "1.0.0-rc2-final",
    "Microsoft.EntityFrameworkCore": "1.0.0",
    "Npgsql.EntityFrameworkCore.PostgreSQL": "1.0.0"
  },
  "frameworks": {
    "netcoreapp1.0": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.0.0"
        },
        "Core": {
          "target": "project"
        },
        "Integration.EntityFramework": {
          "target": "project"
        }
      },
      "imports": "dnxcore50"
    }
  },
  "tools": {
    "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final"
  },
  "publishOptions": {
    "include": [
      "Areas",
      "Views",
      "wwwroot",
      "config.json",
      "web.config"
    ]
  }
}

Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using UI.Helpers;
namespace UI
{
    public class Startup
    {
        public IConfigurationRoot Configuration { get; }
public Startup(IHostingEnvironment env)
        {
            var builder = StartupHelper.GetConfigurationBuilder(env);
            Configuration = builder.Build();
        }
public void Configure(IApplicationBuilder app)
        {
            app.UseMvc();
        }
public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
StartupHelper.AddDatabaseConnectionToServices(services, Configuration);
            StartupHelper.AddDependencyInjectionToServices(services);
        }
    }
}

Вывод

Если вы получите одну вещь от этого предприятия, разделив наш проект на несколько более мелких проектов, это должно заключаться в том, что при разделении вещей у всего есть конкретное место, куда можно пойти. Иметь взаимозаменяемые проекты — это круто, но для меня самое большое преимущество в том, что теперь мне не нужно думать о том, куда что пойдет. Хотите добавить модель представления? UI-проект. Хотите добавить новую модель базы данных? Проект базы данных.

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

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

Весь код, который был рассмотрен в этом посте, можно найти в репозитории на моей странице github.