...i jak nam w tym pomógł message-passing
oraz feature-driven development
/frydrychewicz
/thecorrado
import {route, inject, translated, customElement} from 'app/app';
import {actions} from '../auth.store';
import {Dispatcher} from 'app/core/flux';
import {User} from '../user';
import {DeskId} from 'app/structure';
import {Alert} from 'app/common/ui';
...
@customElement('search-agents')
@inject(CitiesResource, Dispatcher, State, StatesResource, AgencyInformationResource, Moment)
export class SearchAgentsComponent {
public city: string;
public selectedCity: string;
public selectedState: State;
public states: State[];
public agents: ItWorks.Api.Agency.AgentInformation[];
@bindable.expression public country: string;
@bindable.value public size: number;
constructor(
private citiesResource: CitiesResource,
private dispatcher: Dispatcher,
private state: State,
private statesResource: StatesResource,
private agencyInformationResource: AgencyInformationResource,
private moment: Moment) {}
...
}
import {customElement, inject, bindable, route} from 'app/app';
import {PreferredCountriesResource} from './preferredCountries.resource'
import {AgencyId} from 'app/structure/agency';
import "./preferredCountries.css!";
@customElement('preferred-countries')
@inject(PreferredCountriesResource)
@route('iw.auth.preferredCountries', {
url: '/components/preferred-countries'
})
@translate
export class PreferredCountries {
...
}
import {customAttribute, withoutTemplate, element, bindable, inject} from 'app/app'
import {Parse} from 'app/core/angular';
import {AuthProvider} from './provider';
import * as _ from 'lodash';
@customAttribute('iw-authorize')
@withoutTemplate
@inject(Parse)
export class AuthorizeAttribute {
...
}
import {customElement, inject, bindable, route} from 'app/app';
import {PreferredCountriesResource} from './preferredCountries.resource'
import {AgencyId} from 'app/structure/agency';
import "./preferredCountries.css!";
@customElement('preferred-countries')
@inject(PreferredCountriesResource)
@route('iw.auth.preferredCountries', {
url: '/components/preferred-countries'
})
@translate
export class PreferredCountries {
...
}
@route('iw.auth.login', {
url: '/login'
})
@translated
@inject(Dispatcher, User, Alert)
export class LogIn {
...
}
[
...
{
"name": "iw.auth.login",
"controller": "LogIn",
"module": "app/auth/login/login",
"options": {
"url": "/login"
}
},
...
]
import {DynamicFormsResource} from 'app/common/dynamicForms/dynamicForms.resource';
@customElement('customer-form')
@inject(DynamicFormsResource)
export class CustomerForm {
constructor(private dynamicFormsResource: DynamicFormsResource) {}
activate() {
return this.formSchemaPromise = this.dynamicFormsResource
.getForm(ItWorks.Api.DynamicForms.FormType.Customer)
.then(schema => {
...
});
}
...
}
...
@route('iw.auth.login', {
url: '/login',
version: 1
})
@inject(SomeResource)
export class Login {
...
}
@route('iw.auth.login', {
url: '/login',
version: 2
})
@inject(NewVersionOfSomeResource, AnotherService)
export class Login {
...
}
...i wspieramy IE 8 :-)
Message Passing
IDispatcher
IHandleRequest
IDispatcher
public interface IDispatcher
{
OutEnvelope<TResponse> Dispatch<TRequest, TResponse>(TRequest request)
where TRequest : Request<TResponse>
where TResponse : class;
}
IHandleRequest
public interface IHandleRequest<TMessage, TResponse> : IHandleRequest
where TMessage : Request<TResponse>
where TResponse : class
{
OutEnvelope<TResponse> Handle(TMessage request);
}
IHandleRequest
przykład
public class GetSapleDataHandler
: IHandleRequest<GetSampleData.Request, IEnumerable<string>>
{
private readonly SqlConnection _connection;
...ctor...
public OutEnvelope<IEnumerable<string>> Handle(GetSampleData.Request request)
{
var rows = _connection.Query<SomeRow>(@"
SELECT [Something] FROM [Somewhere]");
if (rows == null)
return OutEnvelope<IEnumerable<string>>.NotFound();
var result = rows.Select(r => r.Something);
new OutEnvelope<IEnumerable<string>>(result); // or just: return result;
}
}
IHandleRequest
wywołuje inny handler
public OutEnvelope<ImportantData> Handle(GetImporantData.Request request)
{
var employeeId = _dispatcher.Dispatch(
new GetEmployeeId.Request(request.SomeId)).Body;
var rows = _connection.Query<SomeRow>(@"
SELECT [Something] FROM [Somewhere] WHERE EmployeeId = @EmployeeId",
new { EmployeeId = employeeId });
(...)
}
IHandleRequest
dekorujemy
public CacheHandler(IHandleRequest<TRequest, TResponse> wrapped,
ICacheRequirement requirement,
ICacheService<TRequest,
OutEnvelope<TResponse>> cacheService)
{
_wrapped = wrapped;
_requirement = requirement;
_cacheService = cacheService;
}
public OutEnvelope<TResponse> Handle(TRequest request)
{
return _cacheService.GetOrAdd(_requirement, request,
() => _wrapped.Handle(request));
}
IHandleRequest
więcej dekorujemy!
public CacheHandler(...)
public AuditHandler(...)
public TracingHandler(...)
public GlimpseHandler(...)
Dekorujemy ze StructureMap
public class GlimpsePolicy : IInterceptorPolicy
{
public string Description { get { return "..."; } }
public IEnumerable<IInterceptor> DetermineInterceptors(
Type type, Instance instance)
{
if (!type.IsHandlerInterfaceType())
{
yield break;
}
var handler = typeof(GlimpseHandlerDecorator<,>)
.MakeGenericType(type.GetGenericArguments());
yield return new DecoratorInterceptor(type, handler);
}
}
Testujemy cały handler
[Test]
public void Some_Test_ThenError()
{
Given( // tworzymy Fakes wszystkich wywołań handlerów w testowanym handlerze
new Fakes.UserInAgency(AgencyId, EmployeeId),
new Fakes.HasPermission(EmployeeId, OrgId, ...),
...etc...
);
When(new SearchCustomer.Request(...)); // request dla testowanego handlera
Then( // oczekujemy rezultatu handlera
new ValidationError("Document.Number", "Number missing"));
}
WebApi - Controller
public class CustomerController : ApiController
{
[Route("api/customer/{CustomerId}")]
[ResponseType(typeof (Customer))]
public HttpResponseMessage Get([FromBodyAndUri] GetCustomer.Request request)
{
...call dispatcher...
}
}
WebApi - BaseController
public class BaseController : ApiContoller
{
protected HttpResponseMessage Handle(object request)
{
if (!ModelState.IsValid)
{ ... }
var dependencies = Request.GetDependencyScope();
var service = (IDispatcher)dependencies
.GetService(typeof (IDispatcher));
var outEnvelope = (IOutEnvelope) service.Dispatch(request);
return Request.CreateResponse(outEnvelope.Code,
new ResponseBody(outEnvelope));
}
}
Mono.Cecil & Fody for the win!
public void Execute()
{
var baseController = ModuleDefinition
.GetTypes().Single(t => t.Name == "BaseController");
var handle = baseController
.Resolve().Methods.Single(m => m.Name == "Handle");
var exampleController = baseController.Resolve().NestedTypes[0];
var exampleAction = exampleController.Methods[0];
var requests = GetRequests(ModuleDefinition).ToArray();
var types = new Dictionary<string, TypeDefinition>();
foreach (var request in requests)
{
TypeDefinition controller;
if (types.TryGetValue(request.Area, out controller) == false)
{ ... }
AddAction(request, controller, exampleAction, handle);
}
}
IHandleRequest
- route attribute
[Authorize]
[Route("Customer", "api/customer", RouteAttribute.Methods.Post)]
public OutEnvelope<EmptyResponse> Handle(SaveCustomer.Request request)
{
(...)
return OutEnvelope<EmptyResponse>.Ok();
}
Mono.Cecil - wygenerowane API Controllers
Fody - co jeszcze?
Feature Toggling - IFeature
public interface IFeature<TContext>
{
bool IsEnabled();
}
Feature Toggling - IToggler
public interface IToggler<TFeature, TContext> where TFeature : IFeature<TContext>
{
TFeature Get(TContext context);
IEnumerable<TFeature> GetAll(TContext context);
}
Feature Toggling - FeatureFinder
public interface IFeatureFinder
{
IEnumerable<TFeature> GetInstances<TFeature, TContext>(TContext context)
where TFeature : IFeature<TContext>;
TFeature GetDefault<TFeature, TContext>(TContext context)
where TFeature : IFeature<TContext>;
}
Implementacja Finder opiera się na przeszukiwaniu kontenera SM
HttpCache
SqlDependency
do unieważniania danych
[Cache("dbo", "SomeTable", "SomeColumnOne", "SomeColumn2")]
public OutEnvelope<GetDesksForAgency.Response>
Handle(GetDesksForAgency.Request request)
{
// ...
}
[NotAuditableOperation]
[SensitiveData]
public interface IId : IEquatable<IId>
{ }
public class DocumentId : Id.IntId<DocumentId>
{ }
[TypeConverter(typeof(DeskIdConverter))]
public class DeskId : Id.IId
{
/// Format: {OrganizationId},{AgencyId},{DeskId}
public static DeskId From(string value)
{ ... }
public static bool TryFrom(string value, out DeskId id)
{ ... }
public class DeskIdConverter : Id.BaseFromStringTypeConverter<DeskId>
{
protected override bool TryParse(string value, out DeskId id)
{
return TryFrom(value, out id);
}
}
}
[PushToClientSide]
public class TransactionDetailResponse
{
public TransactionDetailVisibility ColumnVisibility { get; private set; }
public IEnumerable<TransactionDetail> Details { get; private set; }
public TransactionDetailResponse(TransactionDetailVisibility columnVisibility,
IEnumerable<TransactionDetail> details)
{ ... }
}
export module ItWorks.Api.History.GetTransactionDetail {
export class TransactionDetailResponse {
ColumnVisibility: ItWorks.Api.History.GetTransactionDetail.TransactionDetailVisibility;
Details: ItWorks.Api.History.GetTransactionDetail.TransactionDetail[];
}
export class TransactionDetail {
TransactionType: ItWorks.Api.Transaction.TransactionTypes;
Amounts: ItWorks.Api.History.GetTransactionDetail.Amounts;
...
}
...
}
JetBrains Upsource
...a dlaczego nie Atlassian Crucible?
...dlatego