Incoding Framework — Get started

IncFramework-logo
disclaimer:  данная статья является пошаговым руководством, которое поможет ознакомиться с основными возможностями Incoding Framework. Результатом следования данному руководству будет покрытое юнит-тестами приложение, реализующее работу с БД (CRUD + data filters). О Incoding framework ранее уже были статьи на habrahabr, но в них раскрываются отдельные части инструмента.
Для начала приведем краткое описание фреймворкаIncoding Framework состоит из трех пакетов: Incoding framework — back-end проекта, Incoding Meta Language — front-end проекта и Incoding tests helpers — юнит-тесты для back-end«а. Эти пакеты устанавливаются независимо друг от друга, что позволяет интегрировать фреймворк в проект частями: Вы можете подключить только клиентскую или только серверную часть (тесты очень сильно связаны с серверной частью, поэтому их можно позиционировать как дополнение).
В проектах, написанных на Incoding Framework, в качестве серверной архитектуры используется CQRS. В качестве основного инструмента построения клиентской части используется Incoding Meta Language. В целом Incoding Framework покрывает весь цикл разработки приложения.
Типичный solution, созданный с помощью Incoding Framework,  имеет 3 проекта:

  1. Domain (class library) отвечает за бизнес-логику и работу с базой данных.
  2. UI (ASP.NET MVC project)клиентская часть, основанная на ASP.NET MVC.
  3. UnitTests (class library — юнит-тесты для Domain.


Domain


После установки пакета Incoding framework через Nuget в проект помимо необходимых dll устанавливается файл Bootstrapper.cs. Основная задача этого файла — инициализация приложения: инициализация логирования, регистрация IoC, установка настроек Ajax-запросов и пр. В качестве IoC framework по умолчанию устанавливается StructureMap, однако есть провайдер для Ninject, а также есть возможность написания своих реализаций.

namespace Example.Domain
{
    #region << Using >>

    using System;
    using System.Configuration;
    using System.IO;
    using System.Linq;
    using System.Web.Mvc;
    using FluentNHibernate.Cfg;
    using FluentNHibernate.Cfg.Db;
    using FluentValidation;
    using FluentValidation.Mvc;
    using Incoding.Block.IoC;
    using Incoding.Block.Logging;
    using Incoding.CQRS;
    using Incoding.Data;
    using Incoding.EventBroker;
    using Incoding.Extensions;
    using Incoding.MvcContrib;
    using NHibernate.Tool.hbm2ddl;
    using StructureMap.Graph;

    #endregion

    public static class Bootstrapper
    {
        public static void Start()
        {
            //Initialize LoggingFactory
            LoggingFactory.Instance.Initialize(logging =>
                {
 string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log");
 logging.WithPolicy(policy => policy.For(LogType.Debug).Use(FileLogger.WithAtOnceReplace(path,
                                          () => "Debug_{0}.txt".F(DateTime.Now.ToString("yyyyMMdd")))));
                });

            //Initialize IoCFactory
            IoCFactory.Instance.Initialize(init => 
                 init.WithProvider(new StructureMapIoCProvider(registry =>
                {
                registry.For().Use();
                registry.For().Use();
                registry.For().Singleton().Use();

                //Configure FluentlyNhibernate
                var configure = Fluently
                       .Configure()
                       .Database(MsSqlConfiguration.MsSql2008
                .ConnectionString(ConfigurationManager.ConnectionStrings["Example"].ConnectionString))
                       .Mappings(configuration => configuration.FluentMappings
                                                     .AddFromAssembly(typeof(Bootstrapper).Assembly))
                       .ExposeConfiguration(cfg => new SchemaUpdate(cfg).Execute(false, true))
                       .CurrentSessionContext();

                registry.For()
                        .Singleton()
                        .Use(() => new NhibernateSessionFactory(configure));
                registry.For().Use();
                registry.For().Use();

                //Scan currenlty Assembly and registrations all Validators and Event Subscribers
                registry.Scan(r =>
                                    {
                                r.TheCallingAssembly();
                                r.WithDefaultConventions();
                                r.ConnectImplementationsToTypesClosing(typeof(AbstractValidator<>));
                                r.ConnectImplementationsToTypesClosing(typeof(IEventSubscriber<>));
                                r.AddAllTypesOf();
                                    });
                })));

            ModelValidatorProviders.Providers
                          .Add(new FluentValidationModelValidatorProvider(new IncValidatorFactory()));
            FluentValidationModelValidatorProvider.Configure();

            //Execute all SetUp
            foreach (var setUp in IoCFactory.Instance.ResolveAll().OrderBy(r => r.GetOrder()))
            {
                setUp.Execute();
            }

            var ajaxDef = JqueryAjaxOptions.Default;
            ajaxDef.Cache = false; //Disable Ajax cache
        }
    }
}


Далее в Domain дописываются команды (Command) и запросы (Query), которые выполняют операции с базой данных либо какие-то другие действия, связанные с бизнес-логикой приложения.

UI


Пакет Incoding Meta Language при установке добавляет в проект необходимые dll, а также файлы IncodingStart.cs и DispatcherController.cs (часть MVD) необходимые для работы с Domain.

public static class IncodingStart
{
    public static void source Start()
    {
        Bootstrapper.Start();
        new DispatcherController(); // init routes
    }
}
public class DispatcherController : DispatcherControllerBase
{
    #region Constructors

    public DispatcherController()
            : base(typeof(Bootstrapper).Assembly) { }

    #endregion
}


После установки в UI дописывается клиентская логика с использованием IML.

UnitTests


При установке Incoding tests helpers в проект добавляется файл MSpecAssemblyContext.cs, в котором настраивается connection к тестовой базе данных.

public class MSpecAssemblyContext : IAssemblyContext
{
    #region IAssemblyContext Members

    public void OnAssemblyStart()
    {
        //Настройка подключения к тестовой БД
        var configure = Fluently
                .Configure()
                .Database(MsSqlConfiguration.MsSql2008
                         .ConnectionString(ConfigurationManager.ConnectionStrings["Example_Test"].ConnectionString)
                                            .ShowSql())
                .Mappings(configuration => configuration.FluentMappings.AddFromAssembly(typeof(Bootstrapper).Assembly));

        PleasureForData.StartNhibernate(configure, true);
    }

    public void OnAssemblyComplete() { }

    #endregion
}

Часть 1. Установка.


Итак, приступим к выполнению поставленной в disclamer задаче — начнем писать наше приложение. Первый этап создания приложения — создание структуры solution’а проекта и добавление projects в него. Solution проекта будет называться Example и, как уже было сказано во введении, будет иметь три projects. Начнем с project’а, который будет отвечать за бизнес-логику приложения — с Domain.
Создаем class library Domain.
Domain
Далее перейдем к клиентской части — создаем и устанавливаем как запускаемый пустой проект ASP.NET Web Application UI с сылками на MVC packages.
UI1

UI2
И наконец, добавим class library UnitTests,  отвечающую за юнит-тестирование.
UnitTests
Внимание:  хотя юнит-тесты и не являются обязательной частью приложения, мы рекомендуем Вам всегда покрывать код тестами, так как это позволит в будущем избежать множества проблем с ошибками в коде за счет автоматизации тестирования.

После выполнения всех вышеперечисленных действий должен получится следующий Solution:
Solution
После создания структуры Solution’а необходимо собственно установить пакеты Incoding Framework из Nuget в соответствующие projects.
Установка происходит через Nuget. Для всех projects алгоритм установки один:

  1. Кликните правой кнопкой по проекту и выберите в контекстном меню пункт Manage Nuget Packages…
  2. В поиске введите incoding
  3. Выберите нужный пакет и установите его


Сначала устанавливаем Incoding framework в Domain.
Incoding_framework_1
Далее добавляем в файл Domain → Infrastructure → Bootstrapper.cs ссылку на StructureMap.Graph.
StructureMap_ref

В UI нужно установить два пакета:

  1. Incoding Meta Language
  2. Incoding Meta Language Contrib


Incoding_Meta_Languge
MetaLanguageContrib_install
Внимание:  убедитесь, что для References → System.Web.Mvc.dll свойство «Copy Local» установлено в «true»
Теперь файл Example.UI → Views → Shared → _Layout.cshtml измените таким образом, чтобы он выглядел так:

@using Incoding.MvcContrib



    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

@Html.Incoding().RenderDropDownTemplate()

@RenderBody()














Осталось добавить ссылку на Bootstrapper.cs в файлы Example.UI → App_Start → IncodingStart.cs и Example.UI → Controllers → DispatcherController.cs.
IncodingStart_bootstrapper

DispatcherController_bootstrapper
Внимание: если вы используете MVC5, то для работы framework’а необходимо добавить следующий код в файл Web.config


  
  


Осталось установить Incoding tests helpers в UnitTests и добавить ссылку на Bootstrapper.cs в Example.UnitTests → MSpecAssemblyContext.cs.
Incoding_tests_helpers

MSpecAssemblyContext_bootstrapper
Последний этап подготовки проектов к работе — создание структуры папок для projects.
Добавьте следующие папки в проект Example.Domain:

  1. Operations — command и query проекта
  2. Persistences — сущности для маппинга БД
  3. Specifications — where и order спецификации для фильтрации данных при запросах


Example.Domain_folders
В проекте Example.UnitTests создайте такую же структуру папок как и в Example.Domain.
UnitTests_folders
Для начала создадим БД, с которыми будем работать. Откройте SQL Managment Studio и создайте две базы данных: Example и Example_test.
add_DB
example_db
example_test_db
Для того чтобы работать с БД в необходимо настроить connection. Добавьте в файлы Example.UI → Web.config и Example.UnitTests → app.config connection string к базе данных:

  
    
    
  


В файле Example.Domain → Infrastructure → Bootstrapper.cs зарегистрируйте по ключу «Example» соответствующую строку подключения:

//Configure FluentlyNhibernate
var configure = Fluently
        .Configure()
        .Database(MsSqlConfiguration.MsSql2008.ConnectionString(ConfigurationManager
                                                .ConnectionStrings["Example"].ConnectionString))
        .Mappings(configuration => configuration.FluentMappings
                                                .AddFromAssembly(typeof(Bootstrapper).Assembly))
        .ExposeConfiguration(cfg => new SchemaUpdate(cfg).Execute(false, true))
        .CurrentSessionContext(); //Configure data base


В файле Example.UnitTests → MSpecAssemblyContext.cs  зарегистрируйте по ключу «Example_Test» строку подключения к базе данных для тестов:

//Configure connection to Test data base
var configure = Fluently
        .Configure()
        .Database(MsSqlConfiguration.MsSql2008
             .ConnectionString(ConfigurationManager.ConnectionStrings["Example_Test"].ConnectionString)
        .ShowSql())
        .Mappings(configuration => configuration.FluentMappings
                                                .AddFromAssembly(typeof(Bootstrapper).Assembly));


Внимание: базы данных Example и Example_Test должны существовать.
После выполнения всех приведенных выше действий мы подошли к самой интересной части — написанию кода, реализующего CreateReadUpdateDelete-функционал приложения. Для начала необходимо создать класс сущности, которая будет маппиться на БД. В нашем случае это будет Human.cs, который добавим в папку Example.Domain → Persistences.Human.cs

namespace Example.Domain
{
    #region << Using >>

    using System;
    using Incoding.Data;

    #endregion

    public class Human : IncEntityBase
    {
        #region Properties

        public virtual DateTime Birthday { get; set; }

        public virtual string FirstName { get; set; }

        public virtual string Id { get; set; }

        public virtual string LastName { get; set; }

        public virtual Sex Sex { get; set; }

        #endregion

        #region Nested Classes

        public class Map : NHibernateEntityMap
        {
            #region Constructors

            protected Map()
            {
                IdGenerateByGuid(r => r.Id);
                MapEscaping(r => r.FirstName);
                MapEscaping(r => r.LastName);
                MapEscaping(r => r.Birthday);
                MapEscaping(r => r.Sex);
            }

            #endregion
        }

        #endregion
    }

    public enum Sex
    {
        Male = 1,

        Female = 2
    }
}


Наш класс содержит несколько полей, в которые мы будем записывать данные, и вложенный класс маппинга (class Map).
Заметка: после создания класса Human Вам больше не нужно производить никаких действий (дописывание xml-маппинга) благодаря FluentNhibernate.
Теперь добавим команды (Command) и запросы (Query), которые будут отвечать за реализацию CRUD-операций. Первая комманда будет отвечать за добавление новой или изменение существующей записи типа Human. Комманда довольно простая: мы либо получаем из Repository сущность по ключу (Id), либо, если такой сущности нет, создаем новую. В обоих случаях сущность получает значения, которые указаны в свойствах класса AddOrEditHumanCommand. Добавим файл  Example.Domain → Operations → AddOrEditHumanCommand.cs в проект.AddOrEditHumanCommand.cs

namespace Example.Domain
{
    #region << Using >>

    using System;
    using FluentValidation;
    using Incoding.CQRS;
    using Incoding.Extensions;

    #endregion

    public class AddOrEditHumanCommand : CommandBase
    {
        #region Properties

        public DateTime BirthDay { get; set; }

        public string FirstName { get; set; }

        public string Id { get; set; }

        public string LastName { get; set; }

        public Sex Sex { get; set; }

        #endregion

        public override void Execute()
        {
            var human = Repository.GetById(Id) ?? new Human();

            human.FirstName = FirstName;
            human.LastName = LastName;
            human.Birthday = BirthDay;
            human.Sex = Sex;

            Repository.SaveOrUpdate(human);
        }
    }
}


Следующая часть CRUD — Read — запрос на чтение сущностей из базы. Добавьте файл Example.Domain → Operations → GetPeopleQuery.cs.GetPeopleQuery.cs

namespace Example.Domain
{
    #region << Using >>

    using System.Collections.Generic;
    using System.Linq;
    using Incoding.CQRS;

    #endregion

    public class GetPeopleQuery : QueryBase>
    {
        #region Properties

        public string Keyword { get; set; }

        #endregion

        #region Nested Classes

        public class Response
        {
            #region Properties

            public string Birthday { get; set; }

            public string FirstName { get; set; }

            public string Id { get; set; }

            public string LastName { get; set; }

            public string Sex { get; set; }

            #endregion
        }

        #endregion

        protected override List ExecuteResult()
        {
            return Repository.Query().Select(human => new Response
                                                                 {
                                                                         Id = human.Id,
                                                                         Birthday = human.Birthday.ToShortDateString(),
                                                                         FirstName = human.FirstName,
                                                                         LastName = human.LastName,
                                                                         Sex = human.Sex.ToString()
                                                                 }).ToList();
        }
    }
}


И оставшаяся часть функционала — это Delete — удаление записей из БД по ключу (Id).  Добавьте файл Example.Domain → Operations → DeleteHumanCommand.cs.DeleteHumanCommand.cs

namespace Example.Domain
{
    #region << Using >>

    using Incoding.CQRS;

    #endregion

    public class DeleteHumanCommand : CommandBase
    {
        #region Properties

        public string HumanId { get; set; }

        #endregion

        public override void Execute()
        {
            Repository.Delete(HumanId);
        }
    }
}


Для того чтобы наполнить БД начальными данными добавьте файл Example.Domain → InitPeople.cs  — этот файл наследуется от интерфейса ISetUp.ISetup

using System;

namespace Incoding.CQRS
{
  public interface ISetUp : IDisposable
  {
    int GetOrder();

    void Execute();
  }
}


Все экземпляры классов, унаследованных от ISetUp, регистрируются через IoC в Bootstrapper.cs (был приведен во введении). После регистрации они запускаются на исполнение (public void Execute ()) по порядку (public int GetOrder ()).InitPeople.cs

namespace Example.Domain
{
    #region << Using >>

    using System;
    using Incoding.Block.IoC;
    using Incoding.CQRS;
    using NHibernate.Util;

    #endregion

    public class InitPeople : ISetUp
    {
        public void Dispose() { }

        public int GetOrder()
        {
            return 0;
        }

        public void Execute()
        {
            //получение Dispatcher для выполнения Query и Command
            var dispatcher = IoCFactory.Instance.TryResolve();
            
            //не добавлять записи, если в базе есть хотя бы одна запись
            if (dispatcher.Query(new GetEntitiesQuery()).Any())
                return;

            //добавление записей
            dispatcher.Push(new AddOrEditHumanCommand
                                {
                                        FirstName = "Hellen",
                                        LastName = "Jonson",
                                        BirthDay = Convert.ToDateTime("06/05/1985"),
                                        Sex = Sex.Female
                                });
            dispatcher.Push(new AddOrEditHumanCommand
                                {
                                        FirstName = "John",
                                        LastName = "Carlson",
                                        BirthDay = Convert.ToDateTime("06/07/1985"),
                                        Sex = Sex.Male
                                });
        }
    }
}


Back-end реализация CRUD готова. Теперь надо добавить клиентский код. Также как и в случае с серверной частью, начнем реализацию с части создания/редактирования записи. Добавьте файл Example.UI → Views → Home → AddOrEditHuman.cshtml. Представленный IML-код создает стандартную html-форму и работает с командой AddOrEditHumanCommand, отправляя на сервер соответствующий Ajax-запрос.AddOrEditHuman.cshtml

@using Example.Domain
@using Incoding.MetaLanguageContrib
@using Incoding.MvcContrib
@model Example.Domain.AddOrEditHumanCommand
@*Формирование формы для Ajax-отправки на выполнение AddOrEditHumanCommand*@
@using (Html.When(JqueryBind.Submit)
            @*Прерывание поведения по умолчанию и отправка формы через Ajax*@
            .PreventDefault()
            .Submit()
            .OnSuccess(dsl =>
                           {
                               dsl.WithId("PeopleTable").Core().Trigger.Incoding();
                               dsl.WithId("dialog").JqueryUI().Dialog.Close();
                           })
            .OnError(dsl => dsl.Self().Core().Form.Validation.Refresh())
            .AsHtmlAttributes(new
                                  {
                                          action = Url.Dispatcher().Push(new AddOrEditHumanCommand()),
                                          enctype = "multipart/form-data",
                                          method = "POST"
                                  })
            .ToBeginTag(Html, HtmlTag.Form))
{
    
@Html.HiddenFor(r => r.Id) @Html.ForGroup(r => r.FirstName).TextBox(control => control.Label.Name = "First name")
@Html.ForGroup(r => r.LastName).TextBox(control => control.Label.Name = "Last name")
@Html.ForGroup(r => r.BirthDay).TextBox(control => control.Label.Name = "Birthday")
@Html.ForGroup(r => r.Sex).DropDown(control => control.Input.Data = typeof(Sex).ToSelectList())
@*Закрытие диалога*@ @(Html.When(JqueryBind.Click) .PreventDefault() .StopPropagation() .Direct() .OnSuccess(dsl => { dsl.WithId("dialog").JqueryUI().Dialog.Close(); }) .AsHtmlAttributes() .ToButton("Cancel"))
}


Далее следует template, который является шаблоном для загрузки данных, полученных от GetPeopleQuery. Здесь описывается таблица, которая будет отвечать не только за вывод данных, но и за удаление и редактирование отдельных записей: добавьте файл Example.UI → Views → Home → HumanTmpl.cshtml.
HumanTmpl.cshtml

@using Example.Domain
@using Incoding.MetaLanguageContrib
@using Incoding.MvcContrib
@{
    using (var template = Html.Incoding().Template())
    {
        
            @using (var each = template.ForEach())
            {
                
            }
            
First name Last name Birthday Sex
@each.For(r => r.FirstName) @each.For(r => r.LastName) @each.For(r => r.Birthday) @each.For(r => r.Sex) @*Кнопка открытия диалога для редактирования*@ @(Html.When(JqueryBind.Click) .AjaxGet(Url.Dispatcher().Model(new { Id = each.For(r => r.Id), FirstName = each.For(r => r.FirstName), LastName = each.For(r => r.LastName), BirthDay = each.For(r => r.Birthday), Sex = each.For(r => r.Sex) }) .AsView("~/Views/Home/AddOrEditHuman.cshtml")) .OnSuccess(dsl => dsl.WithId("dialog").Behaviors(inDsl => { inDsl.Core().Insert.Html(); inDsl.JqueryUI().Dialog.Open(option => { option.Resizable = false; option.Title = "Edit human"; }); })) .AsHtmlAttributes() .ToButton("Edit")) @*Кнопка удаления записи*@ @(Html.When(JqueryBind.Click) .AjaxPost(Url.Dispatcher().Push(new DeleteHumanCommand() { HumanId = each.For(r => r.Id) })) .OnSuccess(dsl => dsl.WithId("PeopleTable").Core().Trigger.Incoding()) .AsHtmlAttributes() .ToButton("Delete"))
} }


Задача открытия диалогового окна достаточно распространена, поэтому код, отвечающий за это действие, можно вынести в extension.

Последняя часть — изменение стартовой страницы так, чтобы при ее загрузке выполнялся Ajax-запрос на сервер для получения данных от GetPeopleQuery и отображения их через HumanTmpl: измените файл Example.UI → Views → Home → Index.cshtml так, чтобы он соответствовал представленному ниже коду.

Index.cshtml

@using Example.Domain
@using Incoding.MetaLanguageContrib
@using Incoding.MvcContrib
@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}
@*Загрузка записей, полученных из GetPeopleQuery, через HumanTmpl*@ @(Html.When(JqueryBind.InitIncoding) .AjaxGet(Url.Dispatcher().Query(new GetPeopleQuery()).AsJson()) .OnSuccess(dsl => dsl.Self().Core().Insert.WithTemplateByUrl(Url.Dispatcher().AsView("~/Views/Home/HumanTmpl.cshtml")).Html()) .AsHtmlAttributes(new { id = "PeopleTable" }) .ToDiv()) @*Кнопка добавления новой записи*@ @(Html.When(JqueryBind.Click) .AjaxGet(Url.Dispatcher().AsView("~/Views/Home/AddOrEditHuman.cshtml")) .OnSuccess(dsl => dsl.WithId("dialog").Behaviors(inDsl => { inDsl.Core().Insert.Html(); inDsl.JqueryUI().Dialog.Open(option => { option.Resizable = false; option.Title = "Add human"; }); })) .AsHtmlAttributes() .ToButton("Add new human"))


В реальных приложениях валидация введенных данных форм — одна из самых частых задач. Поэтому добавим валидацию данных на форму добавления/редактирования сущности Human. Первая часть — добавление серверного кода.Добавьте следующий код в AddOrEditHumanCommand как nested class:

#region Nested Classes

public class Validator : AbstractValidator
{
    #region Constructors

    public Validator()
    {
        RuleFor(r => r.FirstName).NotEmpty();
        RuleFor(r => r.LastName).NotEmpty();
    }

    #endregion
}

#endregion


На форме AddOrEditHuman.cshtml мы использовали конструкции вида:

@Html.ForGroup()


Поэтому нет необходимости дополнительно добавлять

@Html.ValidationMessageFor()


для полей — ForGroup () сделает это за нас.
Таким образом, мы написали код приложения, которое реализует CRUD-функционал для одной сущности БД.
Еще одна из задач, которые часто встречаются в реальных проектах — фильтрация запрашиваемых данных. В Incoding Framework для удобства написания кода и соблюдения принципа инкапсуляции для фильтрации получаемых в Query данных используются WhereSpecifications. Добавим в написанный код возможность фильтрации получаемых в GetPeopleQuery данных по FirstName и LastName. В первую очередь добавим два файла спецификаций Example.Domain → Specifications → HumanByFirstNameWhereSpec.cs и Example.UI → Specifications → HumanByLastNameWhereSpec.csHumanByFirstNameWhereSpec.cs

namespace Example.Domain
{
    #region << Using >>

    using System;
    using System.Linq.Exsource ssions;
    using Incoding;

    #endregion

    public class HumanByFirstNameWhereSpec : Specification
    {
        #region Fields

        readonly string firstName;

        #endregion

        #region Constructors

        public HumanByFirstNameWhereSpec(string firstName)
        {
            this.firstName = firstName;
        }

        #endregion

        public override Exsource ssion> IsSatisfiedBy()
        {
            if (string.IsNullOrEmpty(this.firstName))
                return null;

            return human => human.FirstName.ToLower().Contains(this.firstName.ToLower());
        }
    }
}


HumanByLastNameWhereSpec.cs

namespace Example.Domain
{
    #region << Using >>

    using System;
    using System.Linq.Exsource ssions;
    using Incoding;

    #endregion

    public class HumanByLastNameWhereSpec : Specification
    {
        #region Fields

        readonly string lastName;

        #endregion

        #region Constructors

        public HumanByLastNameWhereSpec(string lastName)
        {
            this.lastName = lastName.ToLower();
        }

        #endregion

        public override Exsource ssion> IsSatisfiedBy()
        {
            if (string.IsNullOrEmpty(this.lastName))
                return null;

            return human => human.LastName.ToLower().Contains(this.lastName);
        }
    }
}


Теперь используем написанные спецификации в запросе GetPeopleQuery. При помощи связок .Or ()/.And () атомарные спецификации можно соединять в более сложные, что помогает использовать созданные спецификации многократно и при их помощи тонко настраивать необходимые фильтры данных (в нашем примере мы используем связку .Or ()).
GetPeopleQuery.cs

namespace Example.Domain
{
    #region << Using >>

    using System.Collections.Generic;
    using System.Linq;
    using Incoding.CQRS;
    using Incoding.Extensions;

    #endregion

    public class GetPeopleQuery : QueryBase>
    {
        #region Properties

        public string Keyword { get; set; }

        #endregion

        #region Nested Classes

        public class Response
        {
            #region Properties

            public string Birthday { get; set; }

            public string FirstName { get; set; }

            public string Id { get; set; }

            public string LastName { get; set; }

            public string Sex { get; set; }

            #endregion
        }

        #endregion

        protected override List ExecuteResult()
        {
            return Repository.Query(whereSpecification:new HumanByFirstNameWhereSpec(Keyword)
                                                      .Or(new HumanByLastNameWhereSpec(Keyword)))
                             .Select(human => new Response
                                                  {
                                                       Id = human.Id,
                                                       Birthday = human.Birthday.ToShortDateString(),
                                                       FirstName = human.FirstName,
                                                       LastName = human.LastName,
                                                       Sex = human.Sex.ToString()
                                                  })
                             .ToList();
        }
    }
}

И наконец, осталось немного модифицировать Index.cshtml, чтобы добавить поисковую строку, задействующую при запросе поле Keyword для фильтрации данных.

Index.cshtml

@using Example.Domain
@using Incoding.MetaLanguageContrib
@using Incoding.MvcContrib
@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}
@*При нажатии кнопки Find инициируется событие InitIncoding и PeopleTable выполняет запрос GetPeopleQuery с параметром Keyword*@
@(Html.When(JqueryBind.Click) .Direct() .OnSuccess(dsl => dsl.WithId("PeopleTable").Core().Trigger.Incoding()) .AsHtmlAttributes() .ToButton("Find"))
@(Html.When(JqueryBind.InitIncoding) .AjaxGet(Url.Dispatcher().Query(new GetPeopleQuery { Keyword = Selector.Jquery.Id("Keyword") }).AsJson()) .OnSuccess(dsl => dsl.Self().Core().Insert.WithTemplateByUrl(Url.Dispatcher().AsView("~/Views/Home/HumanTmpl.cshtml")).Html()) .AsHtmlAttributes(new { id = "PeopleTable" }) .ToDiv()) @(Html.When(JqueryBind.Click) .AjaxGet(Url.Dispatcher().AsView("~/Views/Home/AddOrEditHuman.cshtml")) .OnSuccess(dsl => dsl.WithId("dialog").Behaviors(inDsl => { inDsl.Core().Insert.Html(); inDsl.JqueryUI().Dialog.Open(option => { option.Resizable = false; option.Title = "Add human"; }); })) .AsHtmlAttributes() .ToButton("Add new human"))


Покроем написанный код тестами. Первый тест отвечает за проверку маппинга сущности Human. Файл When_save_Human.cs добавим в папку Persisteces проекта UnitTests.When_save_Human.cs

namespace Example.UnitTests.Persistences
{
    #region << Using >>

    using Example.Domain;
    using Incoding.MSpecContrib;
    using Machine.Specifications;

    #endregion

    [Subject(typeof(Human))]
    public class When_save_Human : SpecWithPersistenceSpecification
    {
        #region Fields

        It should_be_verify = () => persistenceSpecification.VerifyMappingAndSchema();

        #endregion
    }
}


Данный тест работает с тестовой базой данных (Example_test): создается экземпляр класса Human с автоматически заполненными полями, сохраняется в базу, а затем извлекается и сверяется с созданным экземпляром.
Теперь добавим тесты для WhereSpecifications в папку Specifications.When_human_by_first_name.cs

namespace Example.UnitTests.Specifications
{
    #region << Using >>

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Example.Domain;
    using Incoding.MSpecContrib;
    using Machine.Specifications;

    #endregion

    [Subject(typeof(HumanByFirstNameWhereSpec))]
    public class When_human_by_first_name
    {
        #region Fields

        Establish establish = () =>
                                  {
          Func createEntity = (firstName) =>
               Pleasure.MockStrictAsObject(mock => mock.SetupGet(r => r.FirstName).Returns(firstName));

          fakeCollection = Pleasure.ToQueryable(createEntity(Pleasure.Generator.TheSameString()),
                                                    createEntity(Pleasure.Generator.String()));
                                  };

        Because of = () =>
                         {
          filterCollection = fakeCollection
             .Where(new HumanByFirstNameWhereSpec(Pleasure.Generator.TheSameString()).IsSatisfiedBy())
             .ToList();
                         };

        It should_be_filter = () =>
                                  {
                                      filterCollection.Count.ShouldEqual(1);
                                      filterCollection[0].FirstName.ShouldBeTheSameString();
                                  };

        #endregion

        #region Establish value

        static IQueryable fakeCollection;

        static List filterCollection;

        #endregion
    }
}


When_human_by_last_name.cs

namespace Example.UnitTests.Specifications
{
    #region << Using >>

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Example.Domain;
    using Incoding.MSpecContrib;
    using Machine.Specifications;

    #endregion

    [Subject(typeof(HumanByLastNameWhereSpec))]
    public class When_human_by_last_name
    {
        #region Fields

        Establish establish = () =>
                                  {
          Func createEntity = (lastName) =>
           Pleasure.MockStrictAsObject(mock =>mock.SetupGet(r => r.LastName).Returns(lastName));

          fakeCollection = Pleasure.ToQueryable(createEntity(Pleasure.Generator.TheSameString()),
                                               createEntity(Pleasure.Generator.String()));
                                  };

        Because of = () =>
                         {
           filterCollection = fakeCollection
            .Where(new HumanByLastNameWhereSpec(Pleasure.Generator.TheSameString()).IsSatisfiedBy())
            .ToList();
                         };

        It should_be_filter = () =>
                                  {
                                      filterCollection.Count.ShouldEqual(1);
                                      filterCollection[0].LastName.ShouldBeTheSameString();
                                  };

        #endregion

        #region Establish value

        static IQueryable fakeCollection;

        static List filterCollection;

        #endregion
    }
}


Теперь осталось добавить тесты для команды и запроса (папка Operations), причем для команды необходимо добавить два теста: один для проверки создания новой сущности и второй для проверки редактирования уже существующей сущности.When_get_people_query.cs

namespace Example.UnitTests.Operations
{
    #region << Using >>

    using System.Collections.Generic;
    using Example.Domain;
    using Incoding.Extensions;
    using Incoding.MSpecContrib;
    using Machine.Specifications;

    #endregion

    [Subject(typeof(GetPeopleQuery))]
    public class When_get_people
    {
        #region Fields

        Establish establish = () =>
                                  {
                            var query = Pleasure.Generator.Invent();
                            //Create entity for test with auto-generate
                            human = Pleasure.Generator.Invent();

                            expected = new List();
    
                            mockQuery = MockQuery>
                             .When(query)
                              //"Stub" on query to repository
                             .StubQuery(whereSpecification: 
                                             new HumanByFirstNameWhereSpec(query.Keyword)
                                                  .Or(new HumanByLastNameWhereSpec(query.Keyword)),
                                       entities: human);
                                  };

        Because of = () => mockQuery.Original.Execute();
        
        // Compare result 
        It should_be_result = () => mockQuery
.ShouldBeIsResult(list => list.ShouldEqualWeakEach(new List() { human }, (dsl, i) => 
               dsl.ForwardToValue(r => r.Birthday, human.Birthday.ToShortDateString())
                  .ForwardToValue(r => r.Sex, human.Sex.ToString())));

        #endregion

        #region Establish value

        static MockMessage> mockQuery;

        static List expected;

        static Human human;

        #endregion
    }
}

When_add_human.cs

namespace Example.UnitTests.Operations
{
    #region << Using >>

    using Example.Domain;
    using Incoding.MSpecContrib;
    using Machine.Specifications;

    #endregion

    [Subject(typeof(AddOrEditHumanCommand))]
    public class When_add_human
    {
        #region Fields

        Establish establish = () =>
                                  {
                                      var command = Pleasure.Generator.Invent();

                                      mockCommand = MockCommand
                                              .When(command)
                                              //"Stub" on repository
                                              .StubGetById(command.Id, null);
                                  };

        Because of = () => mockCommand.Original.Execute();

        It should_be_saved = () => mockCommand
            .ShouldBeSaveOrUpdate(human => human.ShouldEqualWeak(mockCommand.Original));

        #endregion

        #region Establish value

        static MockMessage mockCommand;

        #endregion
    }
}


When_edit_human.cs

namespace Example.UnitTests.Operations
{
    #region << Using >>

    using Example.Domain;
    using Incoding.MSpecContrib;
    using Machine.Specifications;

    #endregion

    [Subject(typeof(AddOrEditHumanCommand))]
    public class When_edit_human
    {
        #region Fields

        Establish establish = () =>
                                  {
                                      var command = Pleasure.Generator.Invent();

                                      human = Pleasure.Generator.Invent();

                                      mockCommand = MockCommand
                                              .When(command)
                                              //"Stub" on repository
                                              .StubGetById(command.Id, human);
                                  };

        Because of = () => mockCommand.Original.Execute();

        It should_be_saved = () => mockCommand
                    .ShouldBeSaveOrUpdate(human => human.ShouldEqualWeak(mockCommand.Original));

        #endregion

        #region Establish value

        static MockMessage mockCommand;

        static Human human;

        #endregion
    }
}


  1. CQRS — архитектура серверной части
  2. MVD — описание паттерна Model View Dispatcher
  3. IML introduction
  4. IML TODO MVC

© Habrahabr.ru