diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8d53d7d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,331 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +.artifacts + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ diff --git a/MotoristaAPI.sln b/MotoristaAPI.sln new file mode 100644 index 00000000..a2b58ee1 --- /dev/null +++ b/MotoristaAPI.sln @@ -0,0 +1,87 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29001.49 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{933C500E-FF36-406B-84D6-D84195618050}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Motoristas", "src\Motoristas\Motoristas.csproj", "{D39D3A9B-D7A8-4C39-B24C-5C7FF65D5FC7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroservicesPlatform", "src\MicroservicesPlatform\MicroservicesPlatform.csproj", "{C89D9263-1349-40E7-B64B-F415E1F865A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Motoristas.Handlers", "src\Motoristas.Handlers\Motoristas.Handlers.csproj", "{8B60604F-1F95-4B57-977F-56E67821C8FB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Motoristas.Core", "src\Motoristas.Core\Motoristas.Core.csproj", "{79FB8E3A-A970-4D76-99F2-2E441B108916}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Motoristas.Core.Data.MongoDB", "src\Motoristas.Core.Data.MongoDB\Motoristas.Core.Data.MongoDB.csproj", "{54B28182-F8F7-4F52-984E-80B1A21D5489}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Motoristas.Helpers", "src\Motoristas.Helpers\Motoristas.Helpers.csproj", "{188BE1FC-D607-4616-A098-1E0A27C91671}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Motoristas.Tests", "tests\Motoristas.Tests\Motoristas.Tests.csproj", "{D7AB2A82-74E3-4DAF-91C8-029792968484}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{200838AA-B5DD-459E-8314-030AE1CBA4FD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoogleGeolocationService", "src\GoogleGeolocationService\GoogleGeolocationService.csproj", "{F1AD3084-1A94-4029-956A-2F31AE9418A5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "postman", "postman", "{546EF80D-EBAF-4132-97BB-05FBC0610D81}" + ProjectSection(SolutionItems) = preProject + postman\Dev.postman_environment.json = postman\Dev.postman_environment.json + postman\Perfil.postman_collection.json = postman\Perfil.postman_collection.json + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D39D3A9B-D7A8-4C39-B24C-5C7FF65D5FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D39D3A9B-D7A8-4C39-B24C-5C7FF65D5FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D39D3A9B-D7A8-4C39-B24C-5C7FF65D5FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D39D3A9B-D7A8-4C39-B24C-5C7FF65D5FC7}.Release|Any CPU.Build.0 = Release|Any CPU + {C89D9263-1349-40E7-B64B-F415E1F865A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C89D9263-1349-40E7-B64B-F415E1F865A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C89D9263-1349-40E7-B64B-F415E1F865A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C89D9263-1349-40E7-B64B-F415E1F865A0}.Release|Any CPU.Build.0 = Release|Any CPU + {8B60604F-1F95-4B57-977F-56E67821C8FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B60604F-1F95-4B57-977F-56E67821C8FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B60604F-1F95-4B57-977F-56E67821C8FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B60604F-1F95-4B57-977F-56E67821C8FB}.Release|Any CPU.Build.0 = Release|Any CPU + {79FB8E3A-A970-4D76-99F2-2E441B108916}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79FB8E3A-A970-4D76-99F2-2E441B108916}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79FB8E3A-A970-4D76-99F2-2E441B108916}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79FB8E3A-A970-4D76-99F2-2E441B108916}.Release|Any CPU.Build.0 = Release|Any CPU + {54B28182-F8F7-4F52-984E-80B1A21D5489}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54B28182-F8F7-4F52-984E-80B1A21D5489}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54B28182-F8F7-4F52-984E-80B1A21D5489}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54B28182-F8F7-4F52-984E-80B1A21D5489}.Release|Any CPU.Build.0 = Release|Any CPU + {188BE1FC-D607-4616-A098-1E0A27C91671}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {188BE1FC-D607-4616-A098-1E0A27C91671}.Debug|Any CPU.Build.0 = Debug|Any CPU + {188BE1FC-D607-4616-A098-1E0A27C91671}.Release|Any CPU.ActiveCfg = Release|Any CPU + {188BE1FC-D607-4616-A098-1E0A27C91671}.Release|Any CPU.Build.0 = Release|Any CPU + {D7AB2A82-74E3-4DAF-91C8-029792968484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7AB2A82-74E3-4DAF-91C8-029792968484}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7AB2A82-74E3-4DAF-91C8-029792968484}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7AB2A82-74E3-4DAF-91C8-029792968484}.Release|Any CPU.Build.0 = Release|Any CPU + {F1AD3084-1A94-4029-956A-2F31AE9418A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1AD3084-1A94-4029-956A-2F31AE9418A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1AD3084-1A94-4029-956A-2F31AE9418A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1AD3084-1A94-4029-956A-2F31AE9418A5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D39D3A9B-D7A8-4C39-B24C-5C7FF65D5FC7} = {933C500E-FF36-406B-84D6-D84195618050} + {C89D9263-1349-40E7-B64B-F415E1F865A0} = {933C500E-FF36-406B-84D6-D84195618050} + {8B60604F-1F95-4B57-977F-56E67821C8FB} = {933C500E-FF36-406B-84D6-D84195618050} + {79FB8E3A-A970-4D76-99F2-2E441B108916} = {933C500E-FF36-406B-84D6-D84195618050} + {54B28182-F8F7-4F52-984E-80B1A21D5489} = {933C500E-FF36-406B-84D6-D84195618050} + {188BE1FC-D607-4616-A098-1E0A27C91671} = {933C500E-FF36-406B-84D6-D84195618050} + {D7AB2A82-74E3-4DAF-91C8-029792968484} = {200838AA-B5DD-459E-8314-030AE1CBA4FD} + {F1AD3084-1A94-4029-956A-2F31AE9418A5} = {933C500E-FF36-406B-84D6-D84195618050} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8312EDC4-44EB-49B6-A9B9-FBC4EBE9E1FD} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index 1637538a..943569e7 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,28 @@ -# challenge-dev - Desafio para backend Software Engineer +# Code Challenge Grupo Zap -## Objetivo e avaliação +Para realizar o desafio utilizei o Visual Studio 2019 e criada uma API em ASP.NET Core 2.0 utilizando MediatR. -Objetivo deste teste é avaliar como você irá considerar questões como **arquitetura e design de software, modelagem e aplicação de técnicas e conceitos de programação** e não simplesmente resolver o problema proposto, visto que o mesmo não oferece dificuldades reais para implementação. +# Sobre o código -## Entrega do projeto +O projeto está basicamente dividido em três componentes principais: -Faça um fork deste projeto e ao concluir envie um pull request (PR) com sua implementação. Ao enviar o PR nos informe: +- Motoristas: contém a API que será exposta +- Motorista.Core: contém as entidades de negócio, mapeamentos e as interfaces de Repositório e Serviço de Geolocalização +- Motoristas.Handlers: contém os commands da API, validações e os handlers de implementação dos comandos +- Interface com API de Geolocalização do Google implementada no componentes GoogleGeolocationService +- Projeto MicroservicesPlatform e Motoristas.Helpers dão suporte a infra estrutura necessária para implementação do microserviço e classes úteis sem implementação de regra de negócio +- Motoristas.Core.Data.MongoDB: contém a implementação do DAO (Utilizando MongoDB) - - Explicação sobre a solução desenvolvida (Markdown/Plain Text); - - Quanto tempo você levou para desenvolver a solução. +# Sobre os testes -## Escopo +- Foram implementados dois testes unitários utilizando XUnit: para testar os handlers e para testar o componente responsável pela interface com o Google Geocode API. +- Na pasta Postman os testes das API's com Json de entrada. -Precisamos de uma biblioteca para gerenciar o cadastro de motoristas. +# Sobre o tempo de desenvolvimento: -**Front-end não é necessário e não será avaliado**, porém precisamos de uma API que permita: +Início do desenvolvimento: 11/06 (3 horas) +Dia 2: 13/06 (3 horas) +Dia 3: 14/06 (3 horas) +Dia 4: 15/06 (4 horas) +Último dia de entrega: 16/06 (3 horas) - - criar, editar e excluir um motorista. - -Um cadastro de motorista possui os campos: - - - Nome (primeiro e último); - - Carro (marca, modelo e placa); - - Endereço completo. - -Adicionamente, ao cadastrar um endereço deve ser **buscada as coordenadas** utilizando a [API do Google Maps](https://developers.google.com/maps/documentation/geocoding) e elas devem ser armazenadas junto com o cadastro. - -Além do cadastro também será necessário disponibilizar uma **listagem dos cadastros em ordem alfabética** por nome **ou** sobrenome. - -### Obrigatório - - .NET Core - C# - - WebAPI - - IoC - - Código limpo - -### Desejável - - Documentação da API - - Testes - - Nos informe também o nível de dificuldade e um feedback sobre o desafio proposto [nesta pesquisa](https://goo.gl/forms/qui1n636NKY2w96V2) diff --git a/postman/Dev.postman_environment.json b/postman/Dev.postman_environment.json new file mode 100644 index 00000000..2097066b --- /dev/null +++ b/postman/Dev.postman_environment.json @@ -0,0 +1,27 @@ +{ + "id": "2110f21f-e1dd-476b-ac92-4e51ced2f979", + "name": "Dev", + "values": [ + { + "key": "base_url", + "value": "http://localhost:51000", + "description": { + "content": "", + "type": "text/plain" + }, + "enabled": true + }, + { + "key": "perfilId", + "value": "5c62b888721d2f3c7c82dfe1", + "description": { + "content": "", + "type": "text/plain" + }, + "enabled": true + }, + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2019-02-12T12:58:02.789Z", + "_postman_exported_using": "Postman/6.7.3" +} \ No newline at end of file diff --git a/postman/Perfil.postman_collection.json b/postman/Perfil.postman_collection.json new file mode 100644 index 00000000..8895ef37 --- /dev/null +++ b/postman/Perfil.postman_collection.json @@ -0,0 +1,96 @@ +{ + "info": { + "_postman_id": "dc45f479-af61-4420-9e85-14815c22d92e", + "name": "Wappa_Challenge", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "{{base_url}}/motoristas", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"Nome\": \"PrimeiroNome\",\n\t\"UltimoNome\": \"Sobrenome\",\n\t\"Veiculo\" :\n\t {\n\t \"Marca\": \"MarcaCarro\",\n\t \"Modelo\": \"ModeloCarro\",\n\t \"Placa\": \"PLA-0123\"\n\t },\t\n\t\"Endereco\":\n\t\t{\n\t\t \"Bairro\": \"Bairro Teste\",\n\t\t \"Cep\": \"09812-180\",\n\t\t \"Cidade\": \"Cidade Teste\",\n\t\t \"Complemento\": \"Complemento Teste\",\n\t\t \"Descricao\": \"Avenida Paulista, 1009\",\n\t\t \"Estado\": \"SP\",\n\t\t \"Logradouro\": \"Avenida Paulista, 1009\",\n\t\t \"Numero\" : \"1009A\"\n\t\t} \n}" + }, + "url": { + "raw": "{{base_url}}/motoristas", + "host": [ + "{{base_url}}" + ], + "path": [ + "motoristas" + ] + } + }, + "response": [] + }, + { + "name": "{{base_url}}/motoristas/{{perfilId}}/remover-cadastro", + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "{{base_url}}/motoristas/{{perfilId}}/remover-cadastro", + "host": [ + "{{base_url}}" + ], + "path": [ + "motoristas", + "{{perfilId}}", + "remover-cadastro" + ] + } + }, + "response": [] + }, + { + "name": "{{base_url}}/motoristas/{{perfilId}}", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/motoristas/{{perfilId}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "motoristas", + "{{perfilId}}" + ] + } + }, + "response": [] + }, + { + "name": "{{base_url}}/motoristas?sort=Nome", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/motoristas?sort=Nome", + "host": [ + "{{base_url}}" + ], + "path": [ + "motoristas" + ], + "query": [ + { + "key": "sort", + "value": "Nome" + } + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/src/GoogleGeolocationService/GeolocationService.cs b/src/GoogleGeolocationService/GeolocationService.cs new file mode 100644 index 00000000..6b2bd218 --- /dev/null +++ b/src/GoogleGeolocationService/GeolocationService.cs @@ -0,0 +1,144 @@ +using Flurl; +using Flurl.Http; +using Motoristas.Core; +using Motoristas.Core.Services; +using Serilog; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace GoogleGeolocationService +{ + public class GeolocationService : IGeolocationService + { + private readonly ILogger _log; + private readonly GoogleClientConfig _config; + + public GeolocationService(GoogleClientConfig config, ILogger log) + { + _config = config; + _log = log.ForContext(); + } + + public Coordenadas ObterCoordenadas(string endereco) + { + object GetLogInfo(string enderecoRequest, long elapesedMilliseconds, bool b) + { + return new + { + Url = _config.UriAddress, + TempoConsulta = elapesedMilliseconds, + Status = b ? "Sucesso" : "Erro", + Parametros = new + { + Address = enderecoRequest + } + }; + } + + var sw = Stopwatch.StartNew(); + const string msg = "Consulta Google GeoCoding {@ConsultaGoogle} encerrada."; + + try + { + var data = GetGoogleGeocoding(endereco).Result; + sw.Stop(); + if (data != null) + { + var logInfo = GetLogInfo(endereco, sw.ElapsedMilliseconds, true); + _log.Information(msg, logInfo); + return new Coordenadas(data.RootObjetc.results[0].geometry.location.lat, data.RootObjetc.results[0].geometry.location.lng); + } + else + { + var logInfo = GetLogInfo(endereco, sw.ElapsedMilliseconds, false); + _log.Information(msg, logInfo); + return null; + } + + } + catch(Exception ex) + { + sw.Stop(); + var logInfo = GetLogInfo(endereco, sw.ElapsedMilliseconds, false); + _log.Error(ex, msg, logInfo); + var e = new Exception($"Google geocoding: Erro ao consultar informações (Tempo decorrido: {sw.ElapsedMilliseconds:000}ms).", ex); + throw e; + } + } + + private async Task GetGoogleGeocoding(string address) + { + var result = await _config.UriAddress + .SetQueryParam("address", address) + .SetQueryParam("key", _config.AccessKey) + .GetJsonAsync(); + + _log.Debug("Result {@Result} obtido {Address}", result, address); + + return result; + + } + + private class MapsResponseDto + { + public Rootobject RootObjetc { get; set; } + + public class Rootobject + { + public Result[] results { get; set; } + public string status { get; set; } + } + + public class Result + { + public Address_Components[] address_components { get; set; } + public string formatted_address { get; set; } + public Geometry geometry { get; set; } + public string place_id { get; set; } + public string[] types { get; set; } + } + + public class Geometry + { + public Location location { get; set; } + public string location_type { get; set; } + public Viewport viewport { get; set; } + } + + public class Location + { + public float lat { get; set; } + public float lng { get; set; } + } + + public class Viewport + { + public Northeast northeast { get; set; } + public Southwest southwest { get; set; } + } + + public class Northeast + { + public float lat { get; set; } + public float lng { get; set; } + } + + public class Southwest + { + public float lat { get; set; } + public float lng { get; set; } + } + + public class Address_Components + { + public string long_name { get; set; } + public string short_name { get; set; } + public string[] types { get; set; } + } + + } + } +} diff --git a/src/GoogleGeolocationService/GoogleClientConfig.cs b/src/GoogleGeolocationService/GoogleClientConfig.cs new file mode 100644 index 00000000..e686694d --- /dev/null +++ b/src/GoogleGeolocationService/GoogleClientConfig.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace GoogleGeolocationService +{ + public class GoogleClientConfig + { + public GoogleClientConfig(string uriAddress, string accessKey) + { + UriAddress = uriAddress; + AccessKey = accessKey; + } + + public string UriAddress { get; } + public string AccessKey { get; } + } +} diff --git a/src/GoogleGeolocationService/GoogleGeolocationService.csproj b/src/GoogleGeolocationService/GoogleGeolocationService.csproj new file mode 100644 index 00000000..af08eec6 --- /dev/null +++ b/src/GoogleGeolocationService/GoogleGeolocationService.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp2.1 + + + + + + + + + + + + + diff --git a/src/MicroservicesPlatform/AuthenticationTokenProvider.cs b/src/MicroservicesPlatform/AuthenticationTokenProvider.cs new file mode 100644 index 00000000..ffc5bdd2 --- /dev/null +++ b/src/MicroservicesPlatform/AuthenticationTokenProvider.cs @@ -0,0 +1,94 @@ +using IdentityModel.Client; +using MicroservicesPlatform.Configuration; +using System; +using System.Threading.Tasks; + +namespace MicroservicesPlatform +{ + public interface IAuthTokenProvider + { + Task GetToken(); + Task GetToken(string scope); + } + + public class AuthenticationTokenProvider : IAuthTokenProvider + { + [Obsolete] + private readonly TokenClient _tokenClient; + private readonly string _audience; + + [Obsolete] + public AuthenticationTokenProvider(TokenClient tokenClient, string audience) + { + _tokenClient = tokenClient; + _audience = audience; + } + + [Obsolete] + public Task GetToken() + { + return GetToken(null); + } + + [Obsolete] + public async Task GetToken(string scope) + { + var extra = new { audience = _audience }; + var token = await _tokenClient.RequestClientCredentialsAsync(scope, extra) + .ConfigureAwait(false); + return token; + } + } + + public class AuthenticationTokenProviderFactory : IAuthTokenProviderFactory + { + private static readonly IAuthTokenProviderFactory Singleton = new AuthenticationTokenProviderFactory(); + + private AuthenticationTokenProviderFactory() + { + } + + [Obsolete] + public AuthenticationTokenProvider Create(AuthService config) + { + if (config == null) throw new ArgumentNullException(nameof(config)); + var tokenClient = TokenClientFactory.Instance.Create(config); + var provider = new AuthenticationTokenProvider(tokenClient, config.Audience); + return provider; + } + + public static IAuthTokenProviderFactory Instance => Singleton; + } + + public interface IAuthTokenProviderFactory + { + AuthenticationTokenProvider Create(AuthService config); + } + + public class TokenClientFactory : ITokenClientFactory + { + private static readonly TokenClientFactory Singleton = new TokenClientFactory(); + + private TokenClientFactory() + { + } + + [Obsolete] + public TokenClient Create(AuthService config) + { + if (config == null) throw new ArgumentNullException(nameof(config)); + return new TokenClient(config.Address, + config.ClientId, + config.ClientSecret, + style: AuthenticationStyle.PostValues); + } + + public static ITokenClientFactory Instance => Singleton; + } + + public interface ITokenClientFactory + { + [Obsolete] + TokenClient Create(AuthService config); + } +} diff --git a/src/MicroservicesPlatform/Configuration/CORSConfig.cs b/src/MicroservicesPlatform/Configuration/CORSConfig.cs new file mode 100644 index 00000000..51bfa5e8 --- /dev/null +++ b/src/MicroservicesPlatform/Configuration/CORSConfig.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MicroservicesPlatform.Configuration +{ + public class CORSConfig + { + public string AllowOrigin { get; set; } + } +} diff --git a/src/MicroservicesPlatform/Configuration/Config.cs b/src/MicroservicesPlatform/Configuration/Config.cs new file mode 100644 index 00000000..37de11e8 --- /dev/null +++ b/src/MicroservicesPlatform/Configuration/Config.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MicroservicesPlatform.Configuration +{ + public class DatabaseConfig + { + public string ConnectionString { get; set; } + } + + public class AuthService + { + public string Address { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string Audience { get; set; } + } + + public class AuthenticationConfig + { + public string Authority { get; set; } + public string Audience { get; set; } + } +} diff --git a/src/MicroservicesPlatform/LibOwin.cs b/src/MicroservicesPlatform/LibOwin.cs new file mode 100644 index 00000000..ce491a2d --- /dev/null +++ b/src/MicroservicesPlatform/LibOwin.cs @@ -0,0 +1,3625 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) Damian Hickey. All rights reserved. See License.txt in the project root for license information. + +// https://github.com/damianh/LibOwin +// Modifying this file may result in difficulties when upgrading the package. +// All types are internal. Add a LIBOWIN_PUBLIC compilation symbol to make them public. + +namespace LibOwin.Infrastructure +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + + internal static partial class Constants + { + internal const string Https = "HTTPS"; + + internal const string HttpDateFormat = "r"; + + internal static partial class Headers + { + internal const string ContentType = "Content-Type"; + internal const string CacheControl = "Cache-Control"; + internal const string MediaType = "Media-Type"; + internal const string Accept = "Accept"; + internal const string Host = "Host"; + internal const string ETag = "ETag"; + internal const string Location = "Location"; + internal const string ContentLength = "Content-Length"; + internal const string SetCookie = "Set-Cookie"; + internal const string Expires = "Expires"; + } + } + + internal struct HeaderSegment : IEquatable + { + private readonly StringSegment _formatting; + private readonly StringSegment _data; + + // + // Initializes a new instance of the class. + // + public HeaderSegment(StringSegment formatting, StringSegment data) + { + _formatting = formatting; + _data = data; + } + + public StringSegment Formatting + { + get { return _formatting; } + } + + public StringSegment Data + { + get { return _data; } + } + + #region Equality members + + public bool Equals(HeaderSegment other) + { + return _formatting.Equals(other._formatting) && _data.Equals(other._data); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is HeaderSegment && Equals((HeaderSegment)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (_formatting.GetHashCode() * 397) ^ _data.GetHashCode(); + } + } + + public static bool operator ==(HeaderSegment left, HeaderSegment right) + { + return left.Equals(right); + } + + public static bool operator !=(HeaderSegment left, HeaderSegment right) + { + return !left.Equals(right); + } + + #endregion + } + + internal struct HeaderSegmentCollection : IEnumerable, IEquatable + { + private readonly string[] _headers; + + public HeaderSegmentCollection(string[] headers) + { + _headers = headers; + } + + #region Equality members + + public bool Equals(HeaderSegmentCollection other) + { + return Equals(_headers, other._headers); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is HeaderSegmentCollection && Equals((HeaderSegmentCollection)obj); + } + + public override int GetHashCode() + { + return (_headers != null ? _headers.GetHashCode() : 0); + } + + public static bool operator ==(HeaderSegmentCollection left, HeaderSegmentCollection right) + { + return left.Equals(right); + } + + public static bool operator !=(HeaderSegmentCollection left, HeaderSegmentCollection right) + { + return !left.Equals(right); + } + + #endregion + + public Enumerator GetEnumerator() + { + return new Enumerator(_headers); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + internal struct Enumerator : IEnumerator + { + private readonly string[] _headers; + private int _index; + + private string _header; + private int _headerLength; + private int _offset; + + private int _leadingStart; + private int _leadingEnd; + private int _valueStart; + private int _valueEnd; + private int _trailingStart; + + private Mode _mode; + + private static readonly string[] NoHeaders = new string[0]; + + public Enumerator(string[] headers) + { + _headers = headers ?? NoHeaders; + _header = string.Empty; + _headerLength = -1; + _index = -1; + _offset = -1; + _leadingStart = -1; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + _mode = Mode.Leading; + } + + private enum Mode + { + Leading, + Value, + ValueQuoted, + Trailing, + Produce, + } + + private enum Attr + { + Value, + Quote, + Delimiter, + Whitespace + } + + public HeaderSegment Current + { + get + { + return new HeaderSegment( + new StringSegment(_header, _leadingStart, _leadingEnd - _leadingStart), + new StringSegment(_header, _valueStart, _valueEnd - _valueStart)); + } + } + + object IEnumerator.Current + { + get { return Current; } + } + + public void Dispose() + { + } + + public bool MoveNext() + { + while (true) + { + if (_mode == Mode.Produce) + { + _leadingStart = _trailingStart; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + + if (_offset == _headerLength && + _leadingStart != -1 && + _leadingStart != _offset) + { + // Also produce trailing whitespace + _leadingEnd = _offset; + return true; + } + _mode = Mode.Leading; + } + + // if end of a string + if (_offset == _headerLength) + { + ++_index; + _offset = -1; + _leadingStart = 0; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + + // if that was the last string + if (_index == _headers.Length) + { + // no more move nexts + return false; + } + + // grab the next string + _header = _headers[_index] ?? string.Empty; + _headerLength = _header.Length; + } + while (true) + { + ++_offset; + char ch = _offset == _headerLength ? (char)0 : _header[_offset]; + // todo - array of attrs + Attr attr = char.IsWhiteSpace(ch) ? Attr.Whitespace : ch == '\"' ? Attr.Quote : (ch == ',' || ch == (char)0) ? Attr.Delimiter : Attr.Value; + + switch (_mode) + { + case Mode.Leading: + switch (attr) + { + case Attr.Delimiter: + _leadingEnd = _offset; + _mode = Mode.Produce; + break; + case Attr.Quote: + _leadingEnd = _offset; + _valueStart = _offset; + _mode = Mode.ValueQuoted; + break; + case Attr.Value: + _leadingEnd = _offset; + _valueStart = _offset; + _mode = Mode.Value; + break; + case Attr.Whitespace: + // more + break; + } + break; + case Mode.Value: + switch (attr) + { + case Attr.Quote: + _mode = Mode.ValueQuoted; + break; + case Attr.Delimiter: + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Produce; + break; + case Attr.Value: + // more + break; + case Attr.Whitespace: + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Trailing; + break; + } + break; + case Mode.ValueQuoted: + switch (attr) + { + case Attr.Quote: + _mode = Mode.Value; + break; + case Attr.Delimiter: + if (ch == (char)0) + { + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Produce; + } + break; + case Attr.Value: + case Attr.Whitespace: + // more + break; + } + break; + case Mode.Trailing: + switch (attr) + { + case Attr.Delimiter: + _mode = Mode.Produce; + break; + case Attr.Quote: + // back into value + _trailingStart = -1; + _valueEnd = -1; + _mode = Mode.ValueQuoted; + break; + case Attr.Value: + // back into value + _trailingStart = -1; + _valueEnd = -1; + _mode = Mode.Value; + break; + case Attr.Whitespace: + // more + break; + } + break; + } + if (_mode == Mode.Produce) + { + return true; + } + } + } + } + + public void Reset() + { + _index = 0; + _offset = 0; + _leadingStart = 0; + _leadingEnd = 0; + _valueStart = 0; + _valueEnd = 0; + } + } + } + + internal struct StringSegment : IEquatable + { + private readonly string _buffer; + private readonly int _offset; + private readonly int _count; + + // + // Initializes a new instance of the class. + // + public StringSegment(string buffer, int offset, int count) + { + _buffer = buffer; + _offset = offset; + _count = count; + } + + public string Buffer + { + get { return _buffer; } + } + + public int Offset + { + get { return _offset; } + } + + public int Count + { + get { return _count; } + } + + public string Value + { + get { return _offset == -1 ? null : _buffer.Substring(_offset, _count); } + } + + public bool HasValue + { + get { return _offset != -1 && _count != 0 && _buffer != null; } + } + + #region Equality members + + public bool Equals(StringSegment other) + { + return string.Equals(_buffer, other._buffer) && _offset == other._offset && _count == other._count; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is StringSegment && Equals((StringSegment)obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = (_buffer != null ? _buffer.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ _offset; + hashCode = (hashCode * 397) ^ _count; + return hashCode; + } + } + + public static bool operator ==(StringSegment left, StringSegment right) + { + return left.Equals(right); + } + + public static bool operator !=(StringSegment left, StringSegment right) + { + return !left.Equals(right); + } + + #endregion + + public bool StartsWith(string text, StringComparison comparisonType) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + int textLength = text.Length; + if (!HasValue || _count < textLength) + { + return false; + } + + return string.Compare(_buffer, _offset, text, 0, textLength, comparisonType) == 0; + } + + public bool EndsWith(string text, StringComparison comparisonType) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + int textLength = text.Length; + if (!HasValue || _count < textLength) + { + return false; + } + + return string.Compare(_buffer, _offset + _count - textLength, text, 0, textLength, comparisonType) == 0; + } + + public bool Equals(string text, StringComparison comparisonType) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + int textLength = text.Length; + if (!HasValue || _count != textLength) + { + return false; + } + + return string.Compare(_buffer, _offset, text, 0, textLength, comparisonType) == 0; + } + + public string Substring(int offset, int length) + { + return _buffer.Substring(_offset + offset, length); + } + + public StringSegment Subsegment(int offset, int length) + { + return new StringSegment(_buffer, _offset + offset, length); + } + + public override string ToString() + { + return Value ?? string.Empty; + } + } + + internal static partial class OwinHelpers + { + private static readonly Action AddCookieCallback = (name, value, state) => + { + var dictionary = (IDictionary)state; + if (!dictionary.ContainsKey(name)) + { + dictionary.Add(name, value); + } + }; + + private static readonly char[] SemicolonAndComma = new[] { ';', ',' }; + + internal static IDictionary GetCookies(IOwinRequest request) + { + var cookies = request.Get>("Microsoft.Owin.Cookies#dictionary"); + if (cookies == null) + { + cookies = new Dictionary(StringComparer.Ordinal); + request.Set("Microsoft.Owin.Cookies#dictionary", cookies); + } + + string text = GetHeader(request.Headers, "Cookie"); + if (request.Get("Microsoft.Owin.Cookies#text") != text) + { + cookies.Clear(); + ParseDelimited(text, SemicolonAndComma, AddCookieCallback, cookies); + request.Set("Microsoft.Owin.Cookies#text", text); + } + return cookies; + } + + internal static void ParseDelimited(string text, char[] delimiters, Action callback, object state) + { + int textLength = text.Length; + int equalIndex = text.IndexOf('='); + if (equalIndex == -1) + { + equalIndex = textLength; + } + int scanIndex = 0; + while (scanIndex < textLength) + { + int delimiterIndex = text.IndexOfAny(delimiters, scanIndex); + if (delimiterIndex == -1) + { + delimiterIndex = textLength; + } + if (equalIndex < delimiterIndex) + { + while (scanIndex != equalIndex && char.IsWhiteSpace(text[scanIndex])) + { + ++scanIndex; + } + string name = text.Substring(scanIndex, equalIndex - scanIndex); + string value = text.Substring(equalIndex + 1, delimiterIndex - equalIndex - 1); + callback( + Uri.UnescapeDataString(name.Replace('+', ' ')), + Uri.UnescapeDataString(value.Replace('+', ' ')), + state); + equalIndex = text.IndexOf('=', delimiterIndex); + if (equalIndex == -1) + { + equalIndex = textLength; + } + } + scanIndex = delimiterIndex + 1; + } + } + } + + internal static partial class OwinHelpers + { + public static string GetHeader(IDictionary headers, string key) + { + string[] values = GetHeaderUnmodified(headers, key); + return values == null ? null : string.Join(",", values); + } + + public static IEnumerable GetHeaderSplit(IDictionary headers, string key) + { + string[] values = GetHeaderUnmodified(headers, key); + return values == null ? null : GetHeaderSplitImplementation(values); + } + + private static IEnumerable GetHeaderSplitImplementation(string[] values) + { + foreach (var segment in new HeaderSegmentCollection(values)) + { + if (segment.Data.HasValue) + { + yield return DeQuote(segment.Data.Value); + } + } + } + + public static string[] GetHeaderUnmodified(IDictionary headers, string key) + { + if (headers == null) + { + throw new ArgumentNullException("headers"); + } + string[] values; + return headers.TryGetValue(key, out values) ? values : null; + } + + public static void SetHeader(IDictionary headers, string key, string value) + { + if (headers == null) + { + throw new ArgumentNullException("headers"); + } + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException("key"); + } + if (string.IsNullOrWhiteSpace(value)) + { + headers.Remove(key); + } + else + { + headers[key] = new[] { value }; + } + } + + public static void SetHeaderJoined(IDictionary headers, string key, params string[] values) + { + if (headers == null) + { + throw new ArgumentNullException("headers"); + } + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException("key"); + } + if (values == null || values.Length == 0) + { + headers.Remove(key); + } + else + { + headers[key] = new[] { string.Join(",", values.Select(value => QuoteIfNeeded(value))) }; + } + } + + // Quote items that contain comas and are not already quoted. + private static string QuoteIfNeeded(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + // Ignore + } + else if (value.Contains(',')) + { + if (value[0] != '"' || value[value.Length - 1] != '"') + { + value = '"' + value + '"'; + } + } + + return value; + } + + private static string DeQuote(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + // Ignore + } + else if (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"') + { + value = value.Substring(1, value.Length - 2); + } + + return value; + } + + public static void SetHeaderUnmodified(IDictionary headers, string key, params string[] values) + { + if (headers == null) + { + throw new ArgumentNullException("headers"); + } + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException("key"); + } + if (values == null || values.Length == 0) + { + headers.Remove(key); + } + else + { + headers[key] = values; + } + } + + public static void SetHeaderUnmodified(IDictionary headers, string key, IEnumerable values) + { + if (headers == null) + { + throw new ArgumentNullException("headers"); + } + headers[key] = values.ToArray(); + } + + public static void AppendHeader(IDictionary headers, string key, string values) + { + if (string.IsNullOrWhiteSpace(values)) + { + return; + } + + string existing = GetHeader(headers, key); + if (existing == null) + { + SetHeader(headers, key, values); + } + else + { + headers[key] = new[] { existing + "," + values }; + } + } + + public static void AppendHeaderJoined(IDictionary headers, string key, params string[] values) + { + if (values == null || values.Length == 0) + { + return; + } + + string existing = GetHeader(headers, key); + if (existing == null) + { + SetHeaderJoined(headers, key, values); + } + else + { + headers[key] = new[] { existing + "," + string.Join(",", values.Select(value => QuoteIfNeeded(value))) }; + } + } + + public static void AppendHeaderUnmodified(IDictionary headers, string key, params string[] values) + { + if (values == null || values.Length == 0) + { + return; + } + + string[] existing = GetHeaderUnmodified(headers, key); + if (existing == null) + { + SetHeaderUnmodified(headers, key, values); + } + else + { + SetHeaderUnmodified(headers, key, existing.Concat(values)); + } + } + } + + internal static partial class OwinHelpers + { + private static readonly Action AppendItemCallback = (name, value, state) => + { + var dictionary = (IDictionary>)state; + + List existing; + if (!dictionary.TryGetValue(name, out existing)) + { + dictionary.Add(name, new List(1) { value }); + } + else + { + existing.Add(value); + } + }; + + private static readonly char[] AmpersandAndSemicolon = new[] { '&', ';' }; + + internal static IDictionary GetQuery(IOwinRequest request) + { + var query = request.Get>("Microsoft.Owin.Query#dictionary"); + if (query == null) + { + query = new Dictionary(StringComparer.OrdinalIgnoreCase); + request.Set("Microsoft.Owin.Query#dictionary", query); + } + + string text = request.QueryString.Value; + if (request.Get("Microsoft.Owin.Query#text") != text) + { + query.Clear(); + var accumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); + ParseDelimited(text, AmpersandAndSemicolon, AppendItemCallback, accumulator); + foreach (var kv in accumulator) + { + query.Add(kv.Key, kv.Value.ToArray()); + } + request.Set("Microsoft.Owin.Query#text", text); + } + return query; + } + + internal static IFormCollection GetForm(string text) + { + IDictionary form = new Dictionary(StringComparer.OrdinalIgnoreCase); + var accumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); + ParseDelimited(text, new[] { '&' }, AppendItemCallback, accumulator); + foreach (var kv in accumulator) + { + form.Add(kv.Key, kv.Value.ToArray()); + } + return new FormCollection(form); + } + + internal static string GetJoinedValue(IDictionary store, string key) + { + string[] values = GetUnmodifiedValues(store, key); + return values == null ? null : string.Join(",", values); + } + + internal static string[] GetUnmodifiedValues(IDictionary store, string key) + { + if (store == null) + { + throw new ArgumentNullException("store"); + } + string[] values; + return store.TryGetValue(key, out values) ? values : null; + } + } + + internal static partial class OwinHelpers + { + internal static string GetHost(IOwinRequest request) + { + IHeaderDictionary headers = request.Headers; + + string host = GetHeader(headers, "Host"); + if (!string.IsNullOrWhiteSpace(host)) + { + return host; + } + + string localIpAddress = request.LocalIpAddress ?? "localhost"; + var localPort = request.Get(OwinConstants.CommonKeys.LocalPort); + return string.IsNullOrWhiteSpace(localPort) ? localIpAddress : (localIpAddress + ":" + localPort); + } + } +} + +namespace LibOwin +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Security.Claims; + using System.Security.Principal; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using LibOwin.Infrastructure; + +#if LIBOWIN_PUBLIC + + public partial class CookieOptions { } + public partial class FormCollection { } + public partial class HeaderDictionary { } + public partial struct HostString { } + public partial interface IFormCollection { } + public partial interface IHeaderDictionary { } + public partial interface IOwinContext { } + public partial interface IOwinRequest { } + public partial interface IOwinResponse { } + public partial interface IReadableStringCollection { } + public partial class OwinContext { } + public partial class OwinRequest { } + public partial class OwinResponse { } + public partial struct PathString { } + public partial struct QueryString { } + public partial class ReadableStringCollection { } + public partial class RequestCookieCollection { } + public partial class ResponseCookieCollection { } + public partial class IOwinResponseExtension { } + public partial class OwinEnvironmentExtension { } + +#endif + + /// + /// Options used to create a new cookie. + /// + partial class CookieOptions + { + /// + /// Creates a default cookie with a path of '/'. + /// + public CookieOptions() + { + Path = "/"; + } + + /// + /// Gets or sets the domain to associate the cookie with. + /// + /// The domain to associate the cookie with. + public string Domain { get; set; } + + /// + /// Gets or sets the cookie path. + /// + /// The cookie path. + public string Path { get; set; } + + /// + /// Gets or sets the expiration date and time for the cookie. + /// + /// The expiration date and time for the cookie. + public DateTime? Expires { get; set; } + + /// + /// Gets or sets a value that indicates whether to transmit the cookie using Secure Sockets Layer (SSL)—that is, over HTTPS only. + /// + /// true to transmit the cookie only over an SSL connection (HTTPS); otherwise, false. + public bool Secure { get; set; } + + /// + /// Gets or sets a value that indicates whether a cookie is accessible by client-side script. + /// + /// true if a cookie is accessible by client-side script; otherwise, false. + public bool HttpOnly { get; set; } + } + + /// + /// Contains the parsed form values. + /// + partial class FormCollection : ReadableStringCollection, IFormCollection + { + /// + /// Initializes a new instance of the class. + /// + /// The store for the form. + public FormCollection(IDictionary store) + : base(store) + { } + } + + /// + /// Represents a wrapper for owin.RequestHeaders and owin.ResponseHeaders. + /// + partial class HeaderDictionary : IHeaderDictionary + { + /// + /// Initializes a new instance of the class. + /// + /// The underlying data store. + public HeaderDictionary(IDictionary store) + { + if (store == null) + { + throw new ArgumentNullException("store"); + } + + Store = store; + } + + private IDictionary Store { get; set; } + + /// + /// Gets an that contains the keys in the ;. + /// + /// An that contains the keys in the . + public ICollection Keys + { + get { return Store.Keys; } + } + + /// + /// + /// + public ICollection Values + { + get { return Store.Values; } + } + + /// + /// Gets the number of elements contained in the ;. + /// + /// The number of elements contained in the . + public int Count + { + get { return Store.Count; } + } + + /// + /// Gets a value that indicates whether the is in read-only mode. + /// + /// true if the is in read-only mode; otherwise, false. + public bool IsReadOnly + { + get { return Store.IsReadOnly; } + } + + /// + /// Get or sets the associated value from the collection as a single string. + /// + /// The header name. + /// the associated value from the collection as a single string or null if the key is not present. + public string this[string key] + { + get { return Get(key); } + set { Set(key, value); } + } + + /// + /// Throws KeyNotFoundException if the key is not present. + /// + /// The header name. + /// + string[] IDictionary.this[string key] + { + get { return Store[key]; } + set { Store[key] = value; } + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + public IEnumerator> GetEnumerator() + { + return Store.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Get the associated value from the collection as a single string. + /// + /// The header name. + /// the associated value from the collection as a single string or null if the key is not present. + public string Get(string key) + { + return OwinHelpers.GetHeader(Store, key); + } + + /// + /// Get the associated values from the collection without modification. + /// + /// The header name. + /// the associated value from the collection without modification, or null if the key is not present. + public IList GetValues(string key) + { + return OwinHelpers.GetHeaderUnmodified(Store, key); + } + + /// + /// Get the associated values from the collection separated into individual values. + /// Quoted values will not be split, and the quotes will be removed. + /// + /// The header name. + /// the associated values from the collection separated into individual values, or null if the key is not present. + public IList GetCommaSeparatedValues(string key) + { + IEnumerable values = OwinHelpers.GetHeaderSplit(Store, key); + return values == null ? null : values.ToList(); + } + + /// + /// Add a new value. Appends to the header if already present + /// + /// The header name. + /// The header value. + public void Append(string key, string value) + { + OwinHelpers.AppendHeader(Store, key, value); + } + + /// + /// Add new values. Each item remains a separate array entry. + /// + /// The header name. + /// The header values. + public void AppendValues(string key, params string[] values) + { + OwinHelpers.AppendHeaderUnmodified(Store, key, values); + } + + /// + /// Quotes any values containing comas, and then coma joins all of the values with any existing values. + /// + /// The header name. + /// The header values. + public void AppendCommaSeparatedValues(string key, params string[] values) + { + OwinHelpers.AppendHeaderJoined(Store, key, values); + } + + /// + /// Sets a specific header value. + /// + /// The header name. + /// The header value. + public void Set(string key, string value) + { + OwinHelpers.SetHeader(Store, key, value); + } + + /// + /// Sets the specified header values without modification. + /// + /// The header name. + /// The header values. + public void SetValues(string key, params string[] values) + { + OwinHelpers.SetHeaderUnmodified(Store, key, values); + } + + /// + /// Quotes any values containing comas, and then coma joins all of the values. + /// + /// The header name. + /// The header values. + public void SetCommaSeparatedValues(string key, params string[] values) + { + OwinHelpers.SetHeaderJoined(Store, key, values); + } + + /// + /// Adds the given header and values to the collection. + /// + /// The header name. + /// The header values. + public void Add(string key, string[] value) + { + Store.Add(key, value); + } + + /// + /// Determines whether the contains a specific key. + /// + /// The key. + /// true if the contains a specific key; otherwise, false. + public bool ContainsKey(string key) + { + return Store.ContainsKey(key); + } + + /// + /// Removes the given header from the collection. + /// + /// The header name. + /// true if the specified object was removed from the collection; otherwise, false. + public bool Remove(string key) + { + return Store.Remove(key); + } + + /// + /// Retrieves a value from the dictionary. + /// + /// The header name. + /// The value. + /// true if the contains the key; otherwise, false. + public bool TryGetValue(string key, out string[] value) + { + return Store.TryGetValue(key, out value); + } + + /// + /// Adds a new list of items to the collection. + /// + /// The item to add. + public void Add(KeyValuePair item) + { + Store.Add(item); + } + + /// + /// Clears the entire list of objects. + /// + public void Clear() + { + Store.Clear(); + } + + /// + /// Returns a value indicating whether the specified object occurs within this collection. + /// + /// The item. + /// true if the specified object occurs within this collection; otherwise, false. + public bool Contains(KeyValuePair item) + { + return Store.Contains(item); + } + + /// + /// Copies the elements to a one-dimensional Array instance at the specified index. + /// + /// The one-dimensional Array that is the destination of the specified objects copied from the . + /// The zero-based index in at which copying begins. + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + Store.CopyTo(array, arrayIndex); + } + + /// + /// Removes the given item from the the collection. + /// + /// The item. + /// true if the specified object was removed from the collection; otherwise, false. + public bool Remove(KeyValuePair item) + { + return Store.Remove(item); + } + } + + /// + /// Represents the host portion of a Uri can be used to construct Uri's properly formatted and encoded for use in + /// HTTP headers. + /// + partial struct HostString : IEquatable + { + private readonly string _value; + + /// + /// Creates a new HostString without modification. The value should be Unicode rather than punycode, and may have a port. + /// IPv4 and IPv6 addresses are also allowed, and also may have ports. + /// + /// + public HostString(string value) + { + _value = value; + } + + /// + /// Returns the original value from the constructor. + /// + public string Value + { + get { return _value; } + } + + /// + /// Returns the value as normalized by ToUriComponent(). + /// + /// + public override string ToString() + { + return ToUriComponent(); + } + + /// + /// Returns the value properly formatted and encoded for use in a URI in a HTTP header. + /// Any Unicode is converted to punycode. IPv6 addresses will have brackets added if they are missing. + /// + /// + public string ToUriComponent() + { + int index; + if (string.IsNullOrEmpty(_value)) + { + return string.Empty; + } + else if (_value.IndexOf('[') >= 0) + { + // IPv6 in brackets [::1], maybe with port + return _value; + } + else if ((index = _value.IndexOf(':')) >= 0 + && index < _value.Length - 1 + && _value.IndexOf(':', index + 1) >= 0) + { + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + return "[" + _value + "]"; + } + else if (index >= 0) + { + // Has a port + string port = _value.Substring(index); + var mapping = new IdnMapping(); + return mapping.GetAscii(_value, 0, index) + port; + } + else + { + var mapping = new IdnMapping(); + return mapping.GetAscii(_value); + } + } + + /// + /// Creates a new HostString from the given uri component. + /// Any punycode will be converted to Unicode. + /// + /// + /// + public static HostString FromUriComponent(string uriComponent) + { + if (!string.IsNullOrEmpty(uriComponent)) + { + int index; + if (uriComponent.IndexOf('[') >= 0) + { + // IPv6 in brackets [::1], maybe with port + } + else if ((index = uriComponent.IndexOf(':')) >= 0 + && index < uriComponent.Length - 1 + && uriComponent.IndexOf(':', index + 1) >= 0) + { + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + } + else if (uriComponent.IndexOf("xn--", StringComparison.Ordinal) >= 0) + { + // Contains punycode + if (index >= 0) + { + // Has a port + string port = uriComponent.Substring(index); + IdnMapping mapping = new IdnMapping(); + uriComponent = mapping.GetUnicode(uriComponent, 0, index) + port; + } + else + { + IdnMapping mapping = new IdnMapping(); + uriComponent = mapping.GetUnicode(uriComponent); + } + } + } + return new HostString(uriComponent); + } + + /// + /// Creates a new HostString from the host and port of the give Uri instance. + /// Punycode will be converted to Unicode. + /// + /// + /// + public static HostString FromUriComponent(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException("uri"); + } + return new HostString(uri.GetComponents( + UriComponents.NormalizedHost | // Always convert punycode to Unicode. + UriComponents.HostAndPort, UriFormat.Unescaped)); + } + + /// + /// Compares the equality of the Value property, ignoring case. + /// + /// + /// + public bool Equals(HostString other) + { + return string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Compares against the given object only if it is a HostString. + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + return obj is HostString && Equals((HostString)obj); + } + + /// + /// Gets a hash code for the value. + /// + /// + public override int GetHashCode() + { + return (_value != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(_value) : 0); + } + + /// + /// Compares the two instances for equality. + /// + /// + /// + /// + public static bool operator ==(HostString left, HostString right) + { + return left.Equals(right); + } + + /// + /// Compares the two instances for inequality. + /// + /// + /// + /// + public static bool operator !=(HostString left, HostString right) + { + return !left.Equals(right); + } + } + + /// + /// Contains the parsed form values. + /// + partial interface IFormCollection : IReadableStringCollection + { } + + /// + /// Represents a wrapper for owin.RequestHeaders and owin.ResponseHeaders. + /// + partial interface IHeaderDictionary : IReadableStringCollection, IDictionary + { + /// + /// Get or sets the associated value from the collection as a single string. + /// + /// The header name. + /// the associated value from the collection as a single string or null if the key is not present. + new string this[string key] { get; set; } + + /// + /// Get the associated values from the collection separated into individual values. + /// Quoted values will not be split, and the quotes will be removed. + /// + /// The header name. + /// the associated values from the collection separated into individual values, or null if the key is not present. + IList GetCommaSeparatedValues(string key); + + /// + /// Add a new value. Appends to the header if already present + /// + /// The header name. + /// The header value. + void Append(string key, string value); + + /// + /// Add new values. Each item remains a separate array entry. + /// + /// The header name. + /// The header values. + void AppendValues(string key, params string[] values); + + /// + /// Quotes any values containing comas, and then coma joins all of the values with any existing values. + /// + /// The header name. + /// The header values. + void AppendCommaSeparatedValues(string key, params string[] values); + + /// + /// Sets a specific header value. + /// + /// The header name. + /// The header value. + void Set(string key, string value); + + /// + /// Sets the specified header values without modification. + /// + /// The header name. + /// The header values. + void SetValues(string key, params string[] values); + + /// + /// Quotes any values containing comas, and then coma joins all of the values. + /// + /// The header name. + /// The header values. + void SetCommaSeparatedValues(string key, params string[] values); + } + + /// + /// This wraps OWIN environment dictionary and provides strongly typed accessors. + /// + partial interface IOwinContext + { + /// + /// Gets a wrapper exposing request specific properties. + /// + /// A wrapper exposing request specific properties. + IOwinRequest Request { get; } + + /// + /// Gets a wrapper exposing response specific properties. + /// + /// A wrapper exposing response specific properties. + IOwinResponse Response { get; } + + /// + /// Gets the OWIN environment. + /// + /// The OWIN environment. + IDictionary Environment { get; } + + /// + /// Gets or sets the host.TraceOutput environment value. + /// + /// The host.TraceOutput TextWriter. + TextWriter TraceOutput { get; set; } + + /// + /// Gets a value from the OWIN environment, or returns default(T) if not present. + /// + /// The type of the value. + /// The key of the value to get. + /// The value with the specified key or the default(T) if not present. + T Get(string key); + + /// + /// Sets the given key and value in the OWIN environment. + /// + /// The type of the value. + /// The key of the value to set. + /// The value to set. + /// This instance. + IOwinContext Set(string key, T value); + } + + /// + /// This wraps OWIN environment dictionary and provides strongly typed accessors. + /// + partial interface IOwinRequest + { + /// + /// Gets the OWIN environment. + /// + /// The OWIN environment. + IDictionary Environment { get; } + + /// + /// Gets the request context. + /// + /// The request context. + IOwinContext Context { get; } + + /// + /// Gets or set the HTTP method. + /// + /// The HTTP method. + string Method { get; set; } + + /// + /// Gets or set the HTTP request scheme from owin.RequestScheme. + /// + /// The HTTP request scheme from owin.RequestScheme. + string Scheme { get; set; } + + /// + /// Returns true if the owin.RequestScheme is https. + /// + /// true if this request is using https; otherwise, false. + bool IsSecure { get; } + + /// + /// Gets or set the Host header. May include the port. + /// + /// The Host header. + HostString Host { get; set; } + + /// + /// Gets or set the owin.RequestPathBase. + /// + /// The owin.RequestPathBase. + PathString PathBase { get; set; } + + /// + /// Gets or set the request path from owin.RequestPath. + /// + /// The request path from owin.RequestPath. + PathString Path { get; set; } + + /// + /// Gets or set the query string from owin.RequestQueryString. + /// + /// The query string from owin.RequestQueryString. + QueryString QueryString { get; set; } + + /// + /// Gets the query value collection parsed from owin.RequestQueryString. + /// + /// The query value collection parsed from owin.RequestQueryString. + IReadableStringCollection Query { get; } + + /// + /// Gets the uniform resource identifier (URI) associated with the request. + /// + /// The uniform resource identifier (URI) associated with the request. + Uri Uri { get; } + + /// + /// Gets or set the owin.RequestProtocol. + /// + /// The owin.RequestProtocol. + string Protocol { get; set; } + + /// + /// Gets the request headers. + /// + /// The request headers. + IHeaderDictionary Headers { get; } + + /// + /// Gets the collection of Cookies for this request. + /// + /// The collection of Cookies for this request. + RequestCookieCollection Cookies { get; } + + /// + /// Gets or sets the Content-Type header. + /// + /// The Content-Type header. + string ContentType { get; set; } + + /// + /// Gets or sets the Cache-Control header. + /// + /// The Cache-Control header. + string CacheControl { get; set; } + + /// + /// Gets or sets the Media-Type header. + /// + /// The Media-Type header. + string MediaType { get; set; } + + /// + /// Gets or set the Accept header. + /// + /// The Accept header. + string Accept { get; set; } + + /// + /// Gets or set the owin.RequestBody Stream. + /// + /// The owin.RequestBody Stream. + Stream Body { get; set; } + + /// + /// Gets or sets the cancellation token for the request. + /// + /// The cancellation token for the request. + CancellationToken CallCancelled { get; set; } + + /// + /// Gets or set the server.LocalIpAddress. + /// + /// The server.LocalIpAddress. + string LocalIpAddress { get; set; } + + /// + /// Gets or set the server.LocalPort. + /// + /// The server.LocalPort. + int? LocalPort { get; set; } + + /// + /// Gets or set the server.RemoteIpAddress. + /// + /// The server.RemoteIpAddress. + string RemoteIpAddress { get; set; } + + /// + /// Gets or set the server.RemotePort. + /// + /// The server.RemotePort. + int? RemotePort { get; set; } + + /// + /// Gets or set the owin.RequestUser (or gets server.User for non-standard implementations). + /// + /// The server.User. + ClaimsPrincipal User { get; set; } + + /// + /// Asynchronously reads and parses the request body as a form. + /// + /// The parsed form data. + Task ReadFormAsync(); + + /// + /// Gets a value from the OWIN environment, or returns default(T) if not present. + /// + /// The type of the value. + /// The key of the value to get. + /// The value with the specified key or the default(T) if not present. + T Get(string key); + + /// + /// Sets the given key and value in the OWIN environment. + /// + /// The type of the value. + /// The key of the value to set. + /// The value to set. + /// This instance. + IOwinRequest Set(string key, T value); + } + + /// + /// This wraps OWIN environment dictionary and provides strongly typed accessors. + /// + partial interface IOwinResponse + { + /// + /// Gets the OWIN environment. + /// + /// The OWIN environment. + IDictionary Environment { get; } + + /// + /// Gets the request context. + /// + /// The request context. + IOwinContext Context { get; } + + /// + /// Gets or sets the optional owin.ResponseStatusCode. + /// + /// The optional owin.ResponseStatusCode, or 200 if not set. + int StatusCode { get; set; } + + /// + /// Gets or sets the the optional owin.ResponseReasonPhrase. + /// + /// The the optional owin.ResponseReasonPhrase. + string ReasonPhrase { get; set; } + + /// + /// Gets or sets the owin.ResponseProtocol. + /// + /// The owin.ResponseProtocol. + string Protocol { get; set; } + + /// + /// Gets the response header collection. + /// + /// The response header collection. + IHeaderDictionary Headers { get; } + + /// + /// Gets a collection used to manipulate the Set-Cookie header. + /// + /// A collection used to manipulate the Set-Cookie header. + ResponseCookieCollection Cookies { get; } + + /// + /// Gets or sets the Content-Length header. + /// + /// The Content-Length header. + long? ContentLength { get; set; } + + /// + /// Gets or sets the Content-Type header. + /// + /// The Content-Type header. + string ContentType { get; set; } + + /// + /// Gets or sets the Expires header. + /// + /// The Expires header. + DateTimeOffset? Expires { get; set; } + + /// + /// Gets or sets the E-Tag header. + /// + /// The E-Tag header. + string ETag { get; set; } + + /// + /// Gets or sets the owin.ResponseBody Stream. + /// + /// The owin.ResponseBody Stream. + Stream Body { get; set; } + + /// + /// Registers for an event that fires when the response headers are sent. + /// + /// The callback method. + /// The callback state. + void OnSendingHeaders(Action callback, object state); + + /// + /// Sets a 302 response status code and the Location header. + /// + /// The location where to redirect the client. + void Redirect(string location); + + /// + /// Writes the given text to the response body stream using UTF-8. + /// + /// The response data. + void Write(string text); + + /// + /// Writes the given bytes to the response body stream. + /// + /// The response data. + void Write(byte[] data); + + /// + /// Writes the given bytes to the response body stream. + /// + /// The response data. + /// The zero-based byte offset in the parameter at which to begin copying bytes. + /// The number of bytes to write. + void Write(byte[] data, int offset, int count); + + /// + /// Asynchronously writes the given text to the response body stream using UTF-8. + /// + /// The response data. + /// A Task tracking the state of the write operation. + Task WriteAsync(string text); + + /// + /// Asynchronously writes the given text to the response body stream using UTF-8. + /// + /// The response data. + /// A token used to indicate cancellation. + /// A Task tracking the state of the write operation. + Task WriteAsync(string text, CancellationToken token); + + /// + /// Asynchronously writes the given bytes to the response body stream. + /// + /// The response data. + /// A Task tracking the state of the write operation. + Task WriteAsync(byte[] data); + + /// + /// Asynchronously writes the given bytes to the response body stream. + /// + /// The response data. + /// A token used to indicate cancellation. + /// A Task tracking the state of the write operation. + Task WriteAsync(byte[] data, CancellationToken token); + + /// + /// Asynchronously writes the given bytes to the response body stream. + /// + /// The response data. + /// The zero-based byte offset in the parameter at which to begin copying bytes. + /// The number of bytes to write. + /// A token used to indicate cancellation. + /// A Task tracking the state of the write operation. + Task WriteAsync(byte[] data, int offset, int count, CancellationToken token); + + /// + /// Gets a value from the OWIN environment, or returns default(T) if not present. + /// + /// The type of the value. + /// The key of the value to get. + /// The value with the specified key or the default(T) if not present. + T Get(string key); + + /// + /// Sets the given key and value in the OWIN environment. + /// + /// The type of the value. + /// The key of the value to set. + /// The value to set. + /// This instance. + IOwinResponse Set(string key, T value); + } + + /// + /// Accessors for headers, query, forms, etc. + /// + partial interface IReadableStringCollection : IEnumerable> + { + /// + /// Get the associated value from the collection. Multiple values will be merged. + /// Returns null if the key is not present. + /// + /// + /// + string this[string key] { get; } + + // Joined + + /// + /// Get the associated value from the collection. Multiple values will be merged. + /// Returns null if the key is not present. + /// + /// + /// + string Get(string key); + + // Joined + + /// + /// Get the associated values from the collection in their original format. + /// Returns null if the key is not present. + /// + /// + /// + IList GetValues(string key); + + // Raw + } + + internal static class OwinConstants + { + #region OWIN v1.0.0 - 3.2.1. Request Data + + // http://owin.org/spec/owin-1.0.0.html + + public const string RequestScheme = "owin.RequestScheme"; + public const string RequestMethod = "owin.RequestMethod"; + public const string RequestPathBase = "owin.RequestPathBase"; + public const string RequestPath = "owin.RequestPath"; + public const string RequestQueryString = "owin.RequestQueryString"; + public const string RequestProtocol = "owin.RequestProtocol"; + public const string RequestHeaders = "owin.RequestHeaders"; + public const string RequestBody = "owin.RequestBody"; + public const string RequestUser = "owin.RequestUser"; //owin 1.0.1 + + #endregion + + #region OWIN v1.0.0 - 3.2.2. Response Data + + // http://owin.org/spec/owin-1.0.0.html + + public const string ResponseStatusCode = "owin.ResponseStatusCode"; + public const string ResponseReasonPhrase = "owin.ResponseReasonPhrase"; + public const string ResponseProtocol = "owin.ResponseProtocol"; + public const string ResponseHeaders = "owin.ResponseHeaders"; + public const string ResponseBody = "owin.ResponseBody"; + + #endregion + + #region OWIN v1.0.0 - 3.2.3. Other Data + + // http://owin.org/spec/owin-1.0.0.html + + public const string CallCancelled = "owin.CallCancelled"; + + public const string OwinVersion = "owin.Version"; + + #endregion + + #region OWIN Keys for IAppBuilder.Properties + + internal static class Builder + { + public const string AddSignatureConversion = "builder.AddSignatureConversion"; + public const string DefaultApp = "builder.DefaultApp"; + } + + #endregion + + #region OWIN Key Guidelines and Common Keys - 6. Common keys + + // http://owin.org/spec/CommonKeys.html + + internal static class CommonKeys + { + public const string ClientCertificate = "ssl.ClientCertificate"; + public const string RemoteIpAddress = "server.RemoteIpAddress"; + public const string RemotePort = "server.RemotePort"; + public const string LocalIpAddress = "server.LocalIpAddress"; + public const string LocalPort = "server.LocalPort"; + public const string IsLocal = "server.IsLocal"; + public const string TraceOutput = "host.TraceOutput"; + public const string Addresses = "host.Addresses"; + public const string AppName = "host.AppName"; + public const string Capabilities = "server.Capabilities"; + public const string OnSendingHeaders = "server.OnSendingHeaders"; + public const string OnAppDisposing = "host.OnAppDisposing"; + public const string Scheme = "scheme"; + public const string Host = "host"; + public const string Port = "port"; + public const string Path = "path"; + } + + #endregion + + #region SendFiles v0.3.0 + + // http://owin.org/extensions/owin-SendFile-Extension-v0.3.0.htm + + internal static class SendFiles + { + // 3.1. Startup + + public const string Version = "sendfile.Version"; + public const string Support = "sendfile.Support"; + public const string Concurrency = "sendfile.Concurrency"; + + // 3.2. Per Request + + public const string SendAsync = "sendfile.SendAsync"; + } + + #endregion + + #region Opaque v0.3.0 + + // http://owin.org/extensions/owin-OpaqueStream-Extension-v0.3.0.htm + + internal static class OpaqueConstants + { + // 3.1. Startup + public const string Version = "opaque.Version"; + + // 3.2. Per Request + public const string Upgrade = "opaque.Upgrade"; + + // 5. Consumption + public const string Stream = "opaque.Stream"; + // public const string Version = "opaque.Version"; // redundant, declared above + public const string CallCancelled = "opaque.CallCancelled"; + } + + #endregion + + #region WebSocket v0.4.0 + + // http://owin.org/extensions/owin-OpaqueStream-Extension-v0.3.0.htm + + internal static class WebSocket + { + // 3.1. Startup + public const string Version = "websocket.Version"; + + // 3.2. Per Request + public const string Accept = "websocket.Accept"; + + // 4. Accept + public const string SubProtocol = "websocket.SubProtocol"; + + // 5. Consumption + public const string SendAsync = "websocket.SendAsync"; + public const string ReceiveAsync = "websocket.ReceiveAsync"; + public const string CloseAsync = "websocket.CloseAsync"; + // public const string Version = "websocket.Version"; // redundant, declared above + public const string CallCancelled = "websocket.CallCancelled"; + public const string ClientCloseStatus = "websocket.ClientCloseStatus"; + public const string ClientCloseDescription = "websocket.ClientCloseDescription"; + } + + #endregion + + #region Security v0.1.0 + + // http://owin.org/extensions/owin-Security-Extension-v0.1.0.htm + + internal static class Security + { + // 3.2. Per Request + + public const string User = "server.User"; + + public const string Authenticate = "security.Authenticate"; + + // 3.3. Response + + public const string SignIn = "security.SignIn"; + + public const string SignOut = "security.SignOut"; + + public const string SignOutProperties = "security.SignOutProperties"; + + public const string Challenge = "security.Challenge"; + } + + #endregion + } + + /// + /// This wraps OWIN environment dictionary and provides strongly typed accessors. + /// + partial class OwinContext : IOwinContext + { + /// + /// Create a new context with only request and response header collections. + /// + public OwinContext() + { + IDictionary environment = new Dictionary(StringComparer.Ordinal); + environment[OwinConstants.RequestHeaders] = new Dictionary(StringComparer.OrdinalIgnoreCase); + environment[OwinConstants.ResponseHeaders] = new Dictionary(StringComparer.OrdinalIgnoreCase); + Environment = environment; + Request = new OwinRequest(environment); + Response = new OwinResponse(environment); + } + + /// + /// Create a new wrapper. + /// + /// OWIN environment dictionary which stores state information about the request, response and relevant server state. + public OwinContext(IDictionary environment) + { + if (environment == null) + { + throw new ArgumentNullException("environment"); + } + + Environment = environment; + Request = new OwinRequest(environment); + Response = new OwinResponse(environment); + } + + /// + /// Gets a wrapper exposing request specific properties. + /// + /// A wrapper exposing request specific properties. + public virtual IOwinRequest Request { get; private set; } + + /// + /// Gets a wrapper exposing response specific properties. + /// + /// A wrapper exposing response specific properties. + public virtual IOwinResponse Response { get; private set; } + + /// + /// Gets the OWIN environment. + /// + /// The OWIN environment. + public virtual IDictionary Environment { get; private set; } + + /// + /// Gets or sets the host.TraceOutput environment value. + /// + /// The host.TraceOutput TextWriter. + public virtual TextWriter TraceOutput + { + get { return Get(OwinConstants.CommonKeys.TraceOutput); } + set { Set(OwinConstants.CommonKeys.TraceOutput, value); } + } + + /// + /// Gets a value from the OWIN environment, or returns default(T) if not present. + /// + /// The type of the value. + /// The key of the value to get. + /// The value with the specified key or the default(T) if not present. + public virtual T Get(string key) + { + object value; + return Environment.TryGetValue(key, out value) ? (T)value : default(T); + } + + /// + /// Sets the given key and value in the OWIN environment. + /// + /// The type of the value. + /// The key of the value to set. + /// The value to set. + /// This instance. + public virtual IOwinContext Set(string key, T value) + { + Environment[key] = value; + return this; + } + } + + /// + /// This wraps OWIN environment dictionary and provides strongly typed accessors. + /// + partial class OwinRequest : IOwinRequest + { + /// + /// Create a new context with only request and response header collections. + /// + public OwinRequest() + { + IDictionary environment = new Dictionary(StringComparer.Ordinal); + environment[OwinConstants.RequestHeaders] = new Dictionary(StringComparer.OrdinalIgnoreCase); + environment[OwinConstants.ResponseHeaders] = new Dictionary(StringComparer.OrdinalIgnoreCase); + Environment = environment; + } + + /// + /// Create a new environment wrapper exposing request properties. + /// + /// OWIN environment dictionary which stores state information about the request, response and relevant server state. + public OwinRequest(IDictionary environment) + { + if (environment == null) + { + throw new ArgumentNullException("environment"); + } + + Environment = environment; + } + + /// + /// Gets the OWIN environment. + /// + /// The OWIN environment. + public virtual IDictionary Environment { get; private set; } + + /// + /// Gets the request context. + /// + /// The request context. + public virtual IOwinContext Context + { + get { return new OwinContext(Environment); } + } + + /// + /// Gets or set the HTTP method. + /// + /// The HTTP method. + public virtual string Method + { + get { return Get(OwinConstants.RequestMethod); } + set { Set(OwinConstants.RequestMethod, value); } + } + + /// + /// Gets or set the HTTP request scheme from owin.RequestScheme. + /// + /// The HTTP request scheme from owin.RequestScheme. + public virtual string Scheme + { + get { return Get(OwinConstants.RequestScheme); } + set { Set(OwinConstants.RequestScheme, value); } + } + + /// + /// Returns true if the owin.RequestScheme is https. + /// + /// true if this request is using https; otherwise, false. + public virtual bool IsSecure + { + get { return string.Equals(Scheme, Constants.Https, StringComparison.OrdinalIgnoreCase); } + } + + /// + /// Gets or set the Host header. May include the port. + /// + /// The Host header. + public virtual HostString Host + { + get { return new HostString(OwinHelpers.GetHost(this)); } + set { OwinHelpers.SetHeader(RawHeaders, Constants.Headers.Host, value.Value); } + } + + /// + /// Gets or set the owin.RequestPathBase. + /// + /// The owin.RequestPathBase. + public virtual PathString PathBase + { + get { return new PathString(Get(OwinConstants.RequestPathBase)); } + set { Set(OwinConstants.RequestPathBase, value.Value); } + } + + /// + /// Gets or set the request path from owin.RequestPath. + /// + /// The request path from owin.RequestPath. + public virtual PathString Path + { + get { return new PathString(Get(OwinConstants.RequestPath)); } + set { Set(OwinConstants.RequestPath, value.Value); } + } + + /// + /// Gets or set the query string from owin.RequestQueryString. + /// + /// The query string from owin.RequestQueryString. + public virtual QueryString QueryString + { + get { return new QueryString(Get(OwinConstants.RequestQueryString)); } + set { Set(OwinConstants.RequestQueryString, value.Value); } + } + + /// + /// Gets the query value collection parsed from owin.RequestQueryString. + /// + /// The query value collection parsed from owin.RequestQueryString. + public virtual IReadableStringCollection Query + { + get { return new ReadableStringCollection(OwinHelpers.GetQuery(this)); } + } + + /// + /// Gets the uniform resource identifier (URI) associated with the request. + /// + /// The uniform resource identifier (URI) associated with the request. + public virtual Uri Uri + { + get { return new Uri(Scheme + "://" + Host + PathBase + Path + QueryString); } + } + + /// + /// Gets or set the owin.RequestProtocol. + /// + /// The owin.RequestProtocol. + public virtual string Protocol + { + get { return Get(OwinConstants.RequestProtocol); } + set { Set(OwinConstants.RequestProtocol, value); } + } + + /// + /// Gets the request headers. + /// + /// The request headers. + public virtual IHeaderDictionary Headers + { + get { return new HeaderDictionary(RawHeaders); } + } + + private IDictionary RawHeaders + { + get { return Get>(OwinConstants.RequestHeaders); } + } + + /// + /// Gets the collection of Cookies for this request. + /// + /// The collection of Cookies for this request. + public RequestCookieCollection Cookies + { + get { return new RequestCookieCollection(OwinHelpers.GetCookies(this)); } + } + + /// + /// Gets or sets the Content-Type header. + /// + /// The Content-Type header. + public virtual string ContentType + { + get { return OwinHelpers.GetHeader(RawHeaders, Constants.Headers.ContentType); } + set { OwinHelpers.SetHeader(RawHeaders, Constants.Headers.ContentType, value); } + } + + /// + /// Gets or sets the Cache-Control header. + /// + /// The Cache-Control header. + public virtual string CacheControl + { + get { return OwinHelpers.GetHeader(RawHeaders, Constants.Headers.CacheControl); } + set { OwinHelpers.SetHeader(RawHeaders, Constants.Headers.CacheControl, value); } + } + + /// + /// Gets or sets the Media-Type header. + /// + /// The Media-Type header. + public virtual string MediaType + { + get { return OwinHelpers.GetHeader(RawHeaders, Constants.Headers.MediaType); } + set { OwinHelpers.SetHeader(RawHeaders, Constants.Headers.MediaType, value); } + } + + /// + /// Gets or set the Accept header. + /// + /// The Accept header. + public virtual string Accept + { + get { return OwinHelpers.GetHeader(RawHeaders, Constants.Headers.Accept); } + set { OwinHelpers.SetHeader(RawHeaders, Constants.Headers.Accept, value); } + } + + /// + /// Gets or set the owin.RequestBody Stream. + /// + /// The owin.RequestBody Stream. + public virtual Stream Body + { + get { return Get(OwinConstants.RequestBody); } + set { Set(OwinConstants.RequestBody, value); } + } + + /// + /// Gets or sets the cancellation token for the request. + /// + /// The cancellation token for the request. + public virtual CancellationToken CallCancelled + { + get { return Get(OwinConstants.CallCancelled); } + set { Set(OwinConstants.CallCancelled, value); } + } + + /// + /// Gets or set the server.LocalIpAddress. + /// + /// The server.LocalIpAddress. + public virtual string LocalIpAddress + { + get { return Get(OwinConstants.CommonKeys.LocalIpAddress); } + set { Set(OwinConstants.CommonKeys.LocalIpAddress, value); } + } + + /// + /// Gets or set the server.LocalPort. + /// + /// The server.LocalPort. + public virtual int? LocalPort + { + get + { + int value; + if (int.TryParse(LocalPortString, out value)) + { + return value; + } + return null; + } + set + { + if (value.HasValue) + { + LocalPortString = value.Value.ToString(CultureInfo.InvariantCulture); + } + else + { + Environment.Remove(OwinConstants.CommonKeys.LocalPort); + } + } + } + + private string LocalPortString + { + get { return Get(OwinConstants.CommonKeys.LocalPort); } + set { Set(OwinConstants.CommonKeys.LocalPort, value); } + } + + /// + /// Gets or set the server.RemoteIpAddress. + /// + /// The server.RemoteIpAddress. + public virtual string RemoteIpAddress + { + get { return Get(OwinConstants.CommonKeys.RemoteIpAddress); } + set { Set(OwinConstants.CommonKeys.RemoteIpAddress, value); } + } + + /// + /// Gets or set the server.RemotePort. + /// + /// The server.RemotePort. + public virtual int? RemotePort + { + get + { + int value; + if (int.TryParse(RemotePortString, out value)) + { + return value; + } + return null; + } + set + { + if (value.HasValue) + { + RemotePortString = value.Value.ToString(CultureInfo.InvariantCulture); + } + else + { + Environment.Remove(OwinConstants.CommonKeys.RemotePort); + } + } + } + + private string RemotePortString + { + get { return Get(OwinConstants.CommonKeys.RemotePort); } + set { Set(OwinConstants.CommonKeys.RemotePort, value); } + } + + /// + /// Gets or set the owin.RequestUser (or gets server.User for non-standard implementations). + /// + /// The server.User. + public virtual ClaimsPrincipal User + { + get + { + var claimsPrincipal = Get(OwinConstants.RequestUser); + return claimsPrincipal ?? Get(OwinConstants.Security.User) as ClaimsPrincipal; + } + set { Set(OwinConstants.RequestUser, value); } + } + + /// + /// Asynchronously reads and parses the request body as a form. + /// + /// The parsed form data. + public async Task ReadFormAsync() + { + var form = Get("Microsoft.Owin.Form#collection"); + if (form == null) + { + string text; + // Don't close, it prevents re-winding. + using (var reader = new StreamReader(Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 4 * 1024, leaveOpen: true)) + { + text = await reader.ReadToEndAsync(); + } + form = OwinHelpers.GetForm(text); + Set("Microsoft.Owin.Form#collection", form); + } + + return form; + } + + /// + /// Gets a value from the OWIN environment, or returns default(T) if not present. + /// + /// The type of the value. + /// The key of the value to get. + /// The value with the specified key or the default(T) if not present. + public virtual T Get(string key) + { + object value; + return Environment.TryGetValue(key, out value) ? (T)value : default(T); + } + + /// + /// Sets the given key and value in the OWIN environment. + /// + /// The type of the value. + /// The key of the value to set. + /// The value to set. + /// This instance. + public virtual IOwinRequest Set(string key, T value) + { + Environment[key] = value; + return this; + } + } + + /// + /// This wraps OWIN environment dictionary and provides strongly typed accessors. + /// + partial class OwinResponse : IOwinResponse + { + /// + /// Create a new context with only request and response header collections. + /// + public OwinResponse() + { + IDictionary environment = new Dictionary(StringComparer.Ordinal); + environment[OwinConstants.RequestHeaders] = new Dictionary(StringComparer.OrdinalIgnoreCase); + environment[OwinConstants.ResponseHeaders] = new Dictionary(StringComparer.OrdinalIgnoreCase); + Environment = environment; + } + + /// + /// Creates a new environment wrapper exposing response properties. + /// + /// OWIN environment dictionary which stores state information about the request, response and relevant server state. + public OwinResponse(IDictionary environment) + { + if (environment == null) + { + throw new ArgumentNullException("environment"); + } + + Environment = environment; + } + + /// + /// Gets the OWIN environment. + /// + /// The OWIN environment. + public virtual IDictionary Environment { get; private set; } + + /// + /// Gets the request context. + /// + /// The request context. + public virtual IOwinContext Context + { + get { return new OwinContext(Environment); } + } + + /// + /// Gets or sets the optional owin.ResponseStatusCode. + /// + /// The optional owin.ResponseStatusCode, or 200 if not set. + public virtual int StatusCode + { + get { return Get(OwinConstants.ResponseStatusCode, 200); } + set { Set(OwinConstants.ResponseStatusCode, value); } + } + + /// + /// Gets or sets the the optional owin.ResponseReasonPhrase. + /// + /// The the optional owin.ResponseReasonPhrase. + public virtual string ReasonPhrase + { + get { return Get(OwinConstants.ResponseReasonPhrase); } + set { Set(OwinConstants.ResponseReasonPhrase, value); } + } + + /// + /// Gets or sets the owin.ResponseProtocol. + /// + /// The owin.ResponseProtocol. + public virtual string Protocol + { + get { return Get(OwinConstants.ResponseProtocol); } + set { Set(OwinConstants.ResponseProtocol, value); } + } + + /// + /// Gets the response header collection. + /// + /// The response header collection. + public virtual IHeaderDictionary Headers + { + get { return new HeaderDictionary(RawHeaders); } + } + + private IDictionary RawHeaders + { + get { return Get>(OwinConstants.ResponseHeaders); } + } + + /// + /// Gets a collection used to manipulate the Set-Cookie header. + /// + /// A collection used to manipulate the Set-Cookie header. + public virtual ResponseCookieCollection Cookies + { + get { return new ResponseCookieCollection(Headers); } + } + + /// + /// Gets or sets the Content-Length header. + /// + /// The Content-Length header. + public virtual long? ContentLength + { + get + { + long value; + if (long.TryParse(OwinHelpers.GetHeader(RawHeaders, Constants.Headers.ContentLength), out value)) + { + return value; + } + return null; + } + set + { + if (value.HasValue) + { + OwinHelpers.SetHeader(RawHeaders, Constants.Headers.ContentLength, + value.Value.ToString(CultureInfo.InvariantCulture)); + } + else + { + RawHeaders.Remove(Constants.Headers.ContentLength); + } + } + } + + /// + /// Gets or sets the Content-Type header. + /// + /// The Content-Type header. + public virtual string ContentType + { + get { return OwinHelpers.GetHeader(RawHeaders, Constants.Headers.ContentType); } + set { OwinHelpers.SetHeader(RawHeaders, Constants.Headers.ContentType, value); } + } + + /// + /// Gets or sets the Expires header. + /// + /// The Expires header. + public virtual DateTimeOffset? Expires + { + get + { + DateTimeOffset value; + if (DateTimeOffset.TryParse(OwinHelpers.GetHeader(RawHeaders, Constants.Headers.Expires), + CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out value)) + { + return value; + } + return null; + } + set + { + if (value.HasValue) + { + OwinHelpers.SetHeader(RawHeaders, Constants.Headers.Expires, + value.Value.ToString(Constants.HttpDateFormat, CultureInfo.InvariantCulture)); + } + else + { + RawHeaders.Remove(Constants.Headers.Expires); + } + } + } + + /// + /// Gets or sets the E-Tag header. + /// + /// The E-Tag header. + public virtual string ETag + { + get { return OwinHelpers.GetHeader(RawHeaders, Constants.Headers.ETag); } + set { OwinHelpers.SetHeader(RawHeaders, Constants.Headers.ETag, value); } + } + + /// + /// Gets or sets the owin.ResponseBody Stream. + /// + /// The owin.ResponseBody Stream. + public virtual Stream Body + { + get { return Get(OwinConstants.ResponseBody); } + set { Set(OwinConstants.ResponseBody, value); } + } + + /// + /// Registers for an event that fires when the response headers are sent. + /// + /// The callback method. + /// The callback state. + public virtual void OnSendingHeaders(Action callback, object state) + { + var onSendingHeaders = Get, object>>(OwinConstants.CommonKeys.OnSendingHeaders); + if (onSendingHeaders == null) + { + throw new NotSupportedException(Resources.Exception_MissingOnSendingHeaders); + } + onSendingHeaders(callback, state); + } + + /// + /// Sets a 302 response status code and the Location header. + /// + /// The location where to redirect the client. + public virtual void Redirect(string location) + { + StatusCode = 302; + OwinHelpers.SetHeader(RawHeaders, Constants.Headers.Location, location); + } + + /// + /// Writes the given text to the response body stream using UTF-8. + /// + /// The response data. + public virtual void Write(string text) + { + Write(Encoding.UTF8.GetBytes(text)); + } + + /// + /// Writes the given bytes to the response body stream. + /// + /// The response data. + public virtual void Write(byte[] data) + { + Write(data, 0, data == null ? 0 : data.Length); + } + + /// + /// Writes the given bytes to the response body stream. + /// + /// The response data. + /// The zero-based byte offset in the parameter at which to begin copying bytes. + /// The number of bytes to write. + public virtual void Write(byte[] data, int offset, int count) + { + Body.Write(data, offset, count); + } + + /// + /// Asynchronously writes the given text to the response body stream using UTF-8. + /// + /// The response data. + /// A Task tracking the state of the write operation. + public virtual Task WriteAsync(string text) + { + return WriteAsync(text, CancellationToken.None); + } + + /// + /// Asynchronously writes the given text to the response body stream using UTF-8. + /// + /// The response data. + /// A token used to indicate cancellation. + /// A Task tracking the state of the write operation. + public virtual Task WriteAsync(string text, CancellationToken token) + { + return WriteAsync(Encoding.UTF8.GetBytes(text), token); + } + + /// + /// Asynchronously writes the given bytes to the response body stream. + /// + /// The response data. + /// A Task tracking the state of the write operation. + public virtual Task WriteAsync(byte[] data) + { + return WriteAsync(data, CancellationToken.None); + } + + /// + /// Asynchronously writes the given bytes to the response body stream. + /// + /// The response data. + /// A token used to indicate cancellation. + /// A Task tracking the state of the write operation. + public virtual Task WriteAsync(byte[] data, CancellationToken token) + { + return WriteAsync(data, 0, data == null ? 0 : data.Length, token); + } + + /// + /// Asynchronously writes the given bytes to the response body stream. + /// + /// The response data. + /// The zero-based byte offset in the parameter at which to begin copying bytes. + /// The number of bytes to write. + /// A token used to indicate cancellation. + /// A Task tracking the state of the write operation. + public virtual Task WriteAsync(byte[] data, int offset, int count, CancellationToken token) + { + return Body.WriteAsync(data, offset, count, token); + } + + /// + /// Gets a value from the OWIN environment, or returns default(T) if not present. + /// + /// The type of the value. + /// The key of the value to get. + /// The value with the specified key or the default(T) if not present. + public virtual T Get(string key) + { + return Get(key, default(T)); + } + + private T Get(string key, T fallback) + { + object value; + return Environment.TryGetValue(key, out value) ? (T)value : fallback; + } + + /// + /// Sets the given key and value in the OWIN environment. + /// + /// The type of the value. + /// The key of the value to set. + /// The value to set. + /// This instance. + public virtual IOwinResponse Set(string key, T value) + { + Environment[key] = value; + return this; + } + } + + /// + /// Provides correct escaping for Path and PathBase values when needed to reconstruct a request or redirect URI string + /// + partial struct PathString : IEquatable + { + private static readonly Func EscapeDataString = Uri.EscapeDataString; + + /// + /// Represents the empty path. This field is read-only. + /// + public static readonly PathString Empty = new PathString(String.Empty); + + private readonly string _value; + + /// + /// Initialize the path string with a given value. This value must be in un-escaped format. Use + /// PathString.FromUriComponent(value) if you have a path value which is in an escaped format. + /// + /// The unescaped path to be assigned to the Value property. + public PathString(string value) + { + if (!String.IsNullOrEmpty(value) && value[0] != '/') + { + throw new ArgumentException(Resources.Exception_PathMustStartWithSlash, "value"); + } + _value = value; + } + + /// + /// The unescaped path value + /// + public string Value + { + get { return _value; } + } + + /// + /// True if the path is not empty + /// + public bool HasValue + { + get { return !String.IsNullOrEmpty(_value); } + } + + /// + /// Provides the path string escaped in a way which is correct for combining into the URI representation. + /// + /// The escaped path value + public override string ToString() + { + return ToUriComponent(); + } + + /// + /// Provides the path string escaped in a way which is correct for combining into the URI representation. + /// + /// The escaped path value + public string ToUriComponent() + { + if (HasValue) + { + if (RequiresEscaping(_value)) + { + // TODO: Measure the cost of this escaping and consider optimizing. + return String.Join("/", _value.Split('/').Select(EscapeDataString)); + } + return _value; + } + return String.Empty; + } + + // Very conservative, these characters do not need to be escaped in a path. + private static bool RequiresEscaping(string value) + { + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; + // Check conservatively for safe characters. See http://www.ietf.org/rfc/rfc3986.txt + bool safeChar = + (('a' <= c && c <= 'z') + || ('A' <= c && c <= 'Z') + || ('0' <= c && c <= '9') + || c == '/' || c == '-' || c == '_'); + if (!safeChar) + { + return true; + } + } + return false; + } + + /// + /// Returns an PathString given the path as it is escaped in the URI format. The string MUST NOT contain any + /// value that is not a path. + /// + /// The escaped path as it appears in the URI format. + /// The resulting PathString + public static PathString FromUriComponent(string uriComponent) + { + // REVIEW: what is the exactly correct thing to do? + return new PathString(Uri.UnescapeDataString(uriComponent)); + } + + /// + /// Returns an PathString given the path as from a Uri object. Relative Uri objects are not supported. + /// + /// The Uri object + /// The resulting PathString + public static PathString FromUriComponent(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException("uri"); + } + // REVIEW: what is the exactly correct thing to do? + return new PathString("/" + uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)); + } + + /// + /// Checks if this instance starts with or exactly matches the other instance. Only full segments are matched. + /// + /// + /// + public bool StartsWithSegments(PathString other) + { + string value1 = Value ?? String.Empty; + string value2 = other.Value ?? String.Empty; + if (value1.StartsWith(value2, StringComparison.OrdinalIgnoreCase)) + { + return value1.Length == value2.Length || value1[value2.Length] == '/'; + } + return false; + } + + /// + /// Checks if this instance starts with or exactly matches the other instance. Only full segments are matched. + /// + /// + /// Any remaining segments from this instance not included in the other instance. + /// + public bool StartsWithSegments(PathString other, out PathString remaining) + { + string value1 = Value ?? String.Empty; + string value2 = other.Value ?? String.Empty; + if (value1.StartsWith(value2, StringComparison.OrdinalIgnoreCase)) + { + if (value1.Length == value2.Length || value1[value2.Length] == '/') + { + remaining = new PathString(value1.Substring(value2.Length)); + return true; + } + } + remaining = Empty; + return false; + } + + /// + /// Adds two PathString instances into a combined PathString value. + /// + /// The combined PathString value + public PathString Add(PathString other) + { + return new PathString(Value + other.Value); + } + + /// + /// Combines a PathString and QueryString into the joined URI formatted string value. + /// + /// The joined URI formatted string value + public string Add(QueryString other) + { + return ToUriComponent() + other.ToUriComponent(); + } + + /// + /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. + /// + /// The second PathString for comparison. + /// True if both PathString values are equal + public bool Equals(PathString other) + { + return string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Compares this PathString value to another value using a specific StringComparison type + /// + /// The second PathString for comparison + /// The StringComparison type to use + /// True if both PathString values are equal + public bool Equals(PathString other, StringComparison comparisonType) + { + return string.Equals(_value, other._value, comparisonType); + } + + /// + /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. + /// + /// The second PathString for comparison. + /// True if both PathString values are equal + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + return obj is PathString && Equals((PathString)obj, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Returns the hash code for the PathString value. The hash code is provided by the OrdinalIgnoreCase implementation. + /// + /// The hash code + public override int GetHashCode() + { + return (_value != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(_value) : 0); + } + + /// + /// Operator call through to Equals + /// + /// The left parameter + /// The right parameter + /// True if both PathString values are equal + public static bool operator ==(PathString left, PathString right) + { + return left.Equals(right, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Operator call through to Equals + /// + /// The left parameter + /// The right parameter + /// True if both PathString values are not equal + public static bool operator !=(PathString left, PathString right) + { + return !left.Equals(right, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Operator call through to Add + /// + /// The left parameter + /// The right parameter + /// The PathString combination of both values + public static PathString operator +(PathString left, PathString right) + { + return left.Add(right); + } + + /// + /// Operator call through to Add + /// + /// The left parameter + /// The right parameter + /// The PathString combination of both values + public static string operator +(PathString left, QueryString right) + { + return left.Add(right); + } + } + + /// + /// Provides correct handling for QueryString value when needed to reconstruct a request or redirect URI string + /// + partial struct QueryString : IEquatable + { + /// + /// Represents the empty query string. This field is read-only. + /// + public static readonly QueryString Empty = new QueryString(String.Empty); + + private readonly string _value; + + /// + /// Initalize the query string with a given value. This value must be in escaped and delimited format without + /// a leading '?' character. + /// + /// The query string to be assigned to the Value property. + public QueryString(string value) + { + _value = value; + } + + /// + /// Initialize a query string with a single given parameter name and value. The value is + /// + /// The unencoded parameter name + /// The unencoded parameter value + public QueryString(string name, string value) + { + _value = Uri.EscapeDataString(name) + '=' + Uri.EscapeDataString(value); + } + + /// + /// The unescaped query string without the leading '?' character + /// + public string Value + { + get { return _value; } + } + + /// + /// True if the query string is not empty + /// + public bool HasValue + { + get { return !String.IsNullOrWhiteSpace(_value); } + } + + /// + /// Provides the query string escaped in a way which is correct for combining into the URI representation. + /// A leading '?' character will be prepended unless the Value is null or empty. Characters which are potentally + /// dangerous are escaped. + /// + /// The query string value + public override string ToString() + { + return ToUriComponent(); + } + + /// + /// Provides the query string escaped in a way which is correct for combining into the URI representation. + /// A leading '?' character will be prepended unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// + /// The query string value + public string ToUriComponent() + { + // Escape things properly so System.Uri doesn't mis-interpret the data. + return HasValue ? "?" + _value.Replace("#", "%23") : String.Empty; + } + + /// + /// Returns an QueryString given the query as it is escaped in the URI format. The string MUST NOT contain any + /// value that is not a query. + /// + /// The escaped query as it appears in the URI format. + /// The resulting QueryString + public static QueryString FromUriComponent(string uriComponent) + { + if (String.IsNullOrEmpty(uriComponent)) + { + return new QueryString(string.Empty); + } + if (uriComponent[0] != '?') + { + throw new ArgumentException(Resources.Exception_QueryStringMustStartWithDelimiter, "uriComponent"); + } + return new QueryString(uriComponent.Substring(1)); + } + + /// + /// Returns an QueryString given the query as from a Uri object. Relative Uri objects are not supported. + /// + /// The Uri object + /// The resulting QueryString + public static QueryString FromUriComponent(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException("uri"); + } + return new QueryString(uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped)); + } + + /// + /// Indicates whether the current instance is equal to the other instance. + /// + /// + /// + public bool Equals(QueryString other) + { + return string.Equals(_value, other._value); + } + + /// + /// Indicates whether the current instance is equal to the other instance. + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + return obj is QueryString && Equals((QueryString)obj); + } + + /// + /// Returns the hash code for this instance. + /// + /// + public override int GetHashCode() + { + return (_value != null ? _value.GetHashCode() : 0); + } + + /// + /// Compares the two instances for equality. + /// + /// + /// + /// + public static bool operator ==(QueryString left, QueryString right) + { + return left.Equals(right); + } + + /// + /// Compares the two instances for inequality. + /// + /// + /// + /// + public static bool operator !=(QueryString left, QueryString right) + { + return !left.Equals(right); + } + } + + /// + /// Accessors for query, forms, etc. + /// + partial class ReadableStringCollection : IReadableStringCollection + { + /// + /// Create a new wrapper + /// + /// + public ReadableStringCollection(IDictionary store) + { + if (store == null) + { + throw new ArgumentNullException("store"); + } + + Store = store; + } + + private IDictionary Store { get; set; } + + /// + /// Get the associated value from the collection. Multiple values will be merged. + /// Returns null if the key is not present. + /// + /// + /// + public string this[string key] + { + get { return Get(key); } + } + + /// + /// Get the associated value from the collection. Multiple values will be merged. + /// Returns null if the key is not present. + /// + /// + /// + public string Get(string key) + { + return OwinHelpers.GetJoinedValue(Store, key); + } + + /// + /// Get the associated values from the collection in their original format. + /// Returns null if the key is not present. + /// + /// + /// + public IList GetValues(string key) + { + string[] values; + Store.TryGetValue(key, out values); + return values; + } + + /// + /// + /// + /// + public IEnumerator> GetEnumerator() + { + return Store.GetEnumerator(); + } + + /// + /// + /// + /// + IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + /// + /// A wrapper for the request Cookie header + /// + partial class RequestCookieCollection : IEnumerable> + { + /// + /// Create a new wrapper + /// + /// + public RequestCookieCollection(IDictionary store) + { + if (store == null) + { + throw new ArgumentNullException("store"); + } + + Store = store; + } + + private IDictionary Store { get; set; } + + /// + /// Returns null rather than throwing KeyNotFoundException + /// + /// + /// + public string this[string key] + { + get + { + string value; + Store.TryGetValue(key, out value); + return value; + } + } + + /// + /// + /// + /// + public IEnumerator> GetEnumerator() + { + return Store.GetEnumerator(); + } + + /// + /// + /// + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + internal static class Resources + { + internal const string Exception_MissingOnSendingHeaders = "The OWIN key 'server.OnSsoendingHeaders' is not available for this request."; + internal const string Exception_PathMustStartWithSlash = "The path must start with a '/' followed by one or more characters."; + internal const string Exception_QueryStringMustStartWithDelimiter = "The query string must start with a '?' unless null or empty."; + } + + /// + /// A wrapper for the response Set-Cookie header + /// + partial class ResponseCookieCollection + { + /// + /// Create a new wrapper + /// + /// + public ResponseCookieCollection(IHeaderDictionary headers) + { + if (headers == null) + { + throw new ArgumentNullException("headers"); + } + + Headers = headers; + } + + private IHeaderDictionary Headers { get; set; } + + /// + /// Add a new cookie and value + /// + /// + /// + public void Append(string key, string value) + { + Headers.AppendValues(Constants.Headers.SetCookie, Uri.EscapeDataString(key) + "=" + Uri.EscapeDataString(value) + "; path=/"); + } + + /// + /// Add a new cookie + /// + /// + /// + /// + public void Append(string key, string value, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException("options"); + } + + bool domainHasValue = !string.IsNullOrEmpty(options.Domain); + bool pathHasValue = !string.IsNullOrEmpty(options.Path); + bool expiresHasValue = options.Expires.HasValue; + + string setCookieValue = string.Concat( + Uri.EscapeDataString(key), + "=", + Uri.EscapeDataString(value ?? string.Empty), + !domainHasValue ? null : "; domain=", + !domainHasValue ? null : options.Domain, + !pathHasValue ? null : "; path=", + !pathHasValue ? null : options.Path, + !expiresHasValue ? null : "; expires=", + !expiresHasValue ? null : options.Expires.Value.ToString("ddd, dd-MMM-yyyy HH:mm:ss ", CultureInfo.InvariantCulture) + "GMT", + !options.Secure ? null : "; secure", + !options.HttpOnly ? null : "; HttpOnly"); + Headers.AppendValues("Set-Cookie", setCookieValue); + } + + /// + /// Sets an expired cookie + /// + /// + public void Delete(string key) + { + Func predicate = value => value.StartsWith(key + "=", StringComparison.OrdinalIgnoreCase); + + var deleteCookies = new[] { Uri.EscapeDataString(key) + "=; expires=Thu, 01-Jan-1970 00:00:00 GMT" }; + IList existingValues = Headers.GetValues(Constants.Headers.SetCookie); + if (existingValues == null || existingValues.Count == 0) + { + Headers.SetValues(Constants.Headers.SetCookie, deleteCookies); + } + else + { + Headers.SetValues(Constants.Headers.SetCookie, existingValues.Where(value => !predicate(value)).Concat(deleteCookies).ToArray()); + } + } + + /// + /// Sets an expired cookie + /// + /// + /// + public void Delete(string key, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException("options"); + } + + bool domainHasValue = !string.IsNullOrEmpty(options.Domain); + bool pathHasValue = !string.IsNullOrEmpty(options.Path); + + Func rejectPredicate; + if (domainHasValue) + { + rejectPredicate = value => + value.StartsWith(key + "=", StringComparison.OrdinalIgnoreCase) && + value.IndexOf("domain=" + options.Domain, StringComparison.OrdinalIgnoreCase) != -1; + } + else if (pathHasValue) + { + rejectPredicate = value => + value.StartsWith(key + "=", StringComparison.OrdinalIgnoreCase) && + value.IndexOf("path=" + options.Path, StringComparison.OrdinalIgnoreCase) != -1; + } + else + { + rejectPredicate = value => value.StartsWith(key + "=", StringComparison.OrdinalIgnoreCase); + } + + IList existingValues = Headers.GetValues(Constants.Headers.SetCookie); + if (existingValues != null) + { + Headers.SetValues(Constants.Headers.SetCookie, existingValues.Where(value => !rejectPredicate(value)).ToArray()); + } + + Append(key, string.Empty, new CookieOptions + { + Path = options.Path, + Domain = options.Domain, + Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), + }); + } + } + + + static partial class IOwinResponseExtension + { + /// + /// Registers for an event that fires when the response headers are sent. + /// + /// The owin response + /// The callback method. + /// The callback state. + public static void OnSendingHeaders(this IOwinResponse response, Action callback, T state) + { + if (response == null) + { + throw new ArgumentNullException("response"); + } + Action innerCallback = innerState => callback((T)innerState); + response.OnSendingHeaders(innerCallback, state); + } + + } + + static partial class OwinEnvironmentExtension + { + /// + /// Creates an with the environment. + /// + /// Environment to wrap in an . + /// New . + public static OwinContext AsOwinContext(this IDictionary env) + { + return new OwinContext(env); + } + } +} \ No newline at end of file diff --git a/src/MicroservicesPlatform/LogContextTokenProvider.cs b/src/MicroservicesPlatform/LogContextTokenProvider.cs new file mode 100644 index 00000000..58f26e9c --- /dev/null +++ b/src/MicroservicesPlatform/LogContextTokenProvider.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace MicroservicesPlatform +{ + public interface ILogContextTokenProvider + { + ContextToken GetToken(string tokenType); + } + + public class LogContextTokenProvider : ILogContextTokenProvider + { + private readonly IDictionary _context; + + public LogContextTokenProvider(IDictionary context) + { + _context = context; + } + + public ContextToken GetToken(string tokenType) + { + if (tokenType == null) throw new ArgumentNullException(tokenType); + return _context.ContainsKey(tokenType) + ? new ContextToken(tokenType, _context?[tokenType] as string) + : null; + } + } + + public class ContextToken + { + public ContextToken(string type, string value) + { + Type = type; + Value = value; + } + + public string Type { get; } + public string Value { get; } + } + + public static class LogContextTypes + { + public static string CORRELATION_TOKEN = "x-correlation-token"; + public static string USER_SESSION_TOKEN = "x-user-session-token"; + } +} \ No newline at end of file diff --git a/src/MicroservicesPlatform/MicroservicesPlatform.csproj b/src/MicroservicesPlatform/MicroservicesPlatform.csproj new file mode 100644 index 00000000..7881f514 --- /dev/null +++ b/src/MicroservicesPlatform/MicroservicesPlatform.csproj @@ -0,0 +1,14 @@ + + + + netcoreapp2.1 + + + + + + + + + + diff --git a/src/MicroservicesPlatform/Middlewares/BuildFuncExtensions.cs b/src/MicroservicesPlatform/Middlewares/BuildFuncExtensions.cs new file mode 100644 index 00000000..f609ac71 --- /dev/null +++ b/src/MicroservicesPlatform/Middlewares/BuildFuncExtensions.cs @@ -0,0 +1,21 @@ +using Serilog; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace MicroservicesPlatform.Middlewares +{ + using BuildFunc = Action, Task>, Func, Task>>>; + + public static class BuildFuncExtensions + { + public static BuildFunc UseLogging(this BuildFunc buildFunc, ILogger log) + { + buildFunc(next => CorrelationToken.Middleware(next, log)); + buildFunc(next => RequestLogging.Middleware(next, log)); + buildFunc(next => PerformanceLogging.Middleware(next, log)); + return buildFunc; + } + } +} diff --git a/src/MicroservicesPlatform/Middlewares/LoggingMiddleware.cs b/src/MicroservicesPlatform/Middlewares/LoggingMiddleware.cs new file mode 100644 index 00000000..7c344aa4 --- /dev/null +++ b/src/MicroservicesPlatform/Middlewares/LoggingMiddleware.cs @@ -0,0 +1,83 @@ +using LibOwin; +using Serilog; +using Serilog.Context; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; + +namespace MicroservicesPlatform.Middlewares +{ + using AppFunc = Func, Task>; + + public static class RequestLogging + { + public static AppFunc Middleware(AppFunc next, ILogger log) + { + return async env => + { + var owinContext = new OwinContext(env); + log.Debug("Incoming request: {Method}, {Uri}, {Query}, {@Headers}", + owinContext.Request.Uri.GetLeftPart(UriPartial.Path), + owinContext.Request.Uri.Query.Replace("?", string.Empty), + owinContext.Request.Headers); + await next(env); + log.Debug("Outgoing response: {StatusCode}, {@Headers}", + owinContext.Response.StatusCode, + owinContext.Response.Headers); + }; + } + } + + public static class PerformanceLogging + { + public static AppFunc Middleware(AppFunc next, ILogger log) + { + return async env => + { + var stopWatch = Stopwatch.StartNew(); + await next(env).ConfigureAwait(false); + stopWatch.Stop(); + var owinContext = new OwinContext(env); + var uri = new + { + Path = owinContext.Request.Uri.GetLeftPart(UriPartial.Path), + Query = owinContext.Request.Uri.Query.Replace("?", string.Empty) + }; + log.Information("Request: {Method} {@Uri} executed in {RequestTime}ms with HTTP Status {StatusCode}", + owinContext.Request.Method, + uri, + stopWatch.ElapsedMilliseconds, + owinContext.Response.StatusCode); + }; + } + } + + public static class CorrelationToken + { + public static AppFunc Middleware(AppFunc next, ILogger log) + { + return async env => + { + var owinContext = new OwinContext(env); + var key = LogContextTypes.CORRELATION_TOKEN; + var correlationToken = owinContext.Request.Headers[key] ?? Guid.NewGuid().ToString("N"); + owinContext.Set(key, correlationToken); + + using (LogContext.PushProperty("CorrelationToken", correlationToken)) + { + try + { + await next(env).ConfigureAwait(false); + } + catch (Exception ex) + { + log.Error(ex, "Unhandled exception"); + throw; + } + } + }; + } + } +} diff --git a/src/Motoristas.Core.Data.MongoDB/Collections.cs b/src/Motoristas.Core.Data.MongoDB/Collections.cs new file mode 100644 index 00000000..8cf6e0a4 --- /dev/null +++ b/src/Motoristas.Core.Data.MongoDB/Collections.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Motoristas.Core.Data.MongoDB +{ + internal static class Collections + { + public const string PERFIS_MOTORISTA = "perfisMotorista"; + } +} diff --git a/src/Motoristas.Core.Data.MongoDB/IDbContext.cs b/src/Motoristas.Core.Data.MongoDB/IDbContext.cs new file mode 100644 index 00000000..cf9811d7 --- /dev/null +++ b/src/Motoristas.Core.Data.MongoDB/IDbContext.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Motoristas.Core.Data.MongoDB +{ + public interface IDbContext + { + IQueryable Collection { get; } + T Load(string id); + Task Save(T model); + T Delete(string id); + List Find(string sort); + } +} diff --git a/src/Motoristas.Core.Data.MongoDB/IPerfilMotoristaDbContext.cs b/src/Motoristas.Core.Data.MongoDB/IPerfilMotoristaDbContext.cs new file mode 100644 index 00000000..3c551b52 --- /dev/null +++ b/src/Motoristas.Core.Data.MongoDB/IPerfilMotoristaDbContext.cs @@ -0,0 +1,9 @@ + +using Motoristas.Core.States; + +namespace Motoristas.Core.Data.MongoDB +{ + public interface IPerfilMotoristaDbContext : IDbContext + { + } +} \ No newline at end of file diff --git a/src/Motoristas.Core.Data.MongoDB/MongoDbContext.cs b/src/Motoristas.Core.Data.MongoDB/MongoDbContext.cs new file mode 100644 index 00000000..da8225c1 --- /dev/null +++ b/src/Motoristas.Core.Data.MongoDB/MongoDbContext.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver; +using Motoristas.Core.States; + +namespace Motoristas.Core.Data.MongoDB +{ + public abstract class MongoDbContext where T : IStateIdentity + { + private readonly IMongoCollection _mongoCollection; + + static MongoDbContext() + { + var pack = new ConventionPack + { + new CamelCaseElementNameConvention(), + new IgnoreIfDefaultConvention(false), + new IgnoreIfNullConvention(true) + }; + ConventionRegistry.Register("Default conventions", pack, t => true); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdProperty(c => c.Id) + .SetSerializer(new StringSerializer(BsonType.ObjectId)); + }); + + BsonSerializer.RegisterSerializer(typeof(decimal), new DecimalSerializer(BsonType.Decimal128)); + BsonSerializer.RegisterSerializer(typeof(decimal?), new NullableSerializer(new DecimalSerializer(BsonType.Decimal128))); + } + + protected MongoDbContext(string connectionString, string collectionName) + { + var url = MongoUrl.Create(connectionString); + var client = new MongoClient(url); + Database = client.GetDatabase(url.DatabaseName); + _mongoCollection = Database.GetCollection(collectionName); + } + + protected IMongoDatabase Database { get; } + + public IQueryable Collection => _mongoCollection.AsQueryable(); + + public Task Save(T state) + { + var filter = Builders.Filter.Eq(s => s.Id, state.Id); + var updateOptions = new UpdateOptions {IsUpsert = true}; + var result = _mongoCollection.ReplaceOneAsync(filter, state, updateOptions); + return result; + } + + public T Load(string id) + { + var filter = Builders.Filter.Eq(s => s.Id, id); + var result = _mongoCollection.Find(filter).FirstOrDefault(); + return result; + } + + public T Delete(string id) + { + var filter = Builders.Filter.Eq(s => s.Id, id); + var result = _mongoCollection.FindOneAndDelete(filter); + return result; + } + + public List Find(string sort) + { + return _mongoCollection.Find(FilterDefinition.Empty) + .Sort(Builders.Sort.Ascending(sort)) + .ToList(); + + } + } +} diff --git a/src/Motoristas.Core.Data.MongoDB/MongoDbIdentityGenerator.cs b/src/Motoristas.Core.Data.MongoDB/MongoDbIdentityGenerator.cs new file mode 100644 index 00000000..118a078f --- /dev/null +++ b/src/Motoristas.Core.Data.MongoDB/MongoDbIdentityGenerator.cs @@ -0,0 +1,12 @@ +using MongoDB.Bson; + +namespace Motoristas.Core.Data.MongoDB +{ + public class MongoDbIdentityGenerator : IIdentityGenerator + { + public string Create() + { + return ObjectId.GenerateNewId().ToString(); + } + } +} diff --git a/src/Motoristas.Core.Data.MongoDB/Motoristas.Core.Data.MongoDB.csproj b/src/Motoristas.Core.Data.MongoDB/Motoristas.Core.Data.MongoDB.csproj new file mode 100644 index 00000000..046f764f --- /dev/null +++ b/src/Motoristas.Core.Data.MongoDB/Motoristas.Core.Data.MongoDB.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp2.1 + + + + + + + + + + + diff --git a/src/Motoristas.Core.Data.MongoDB/PerfilMotoristaDbContext.cs b/src/Motoristas.Core.Data.MongoDB/PerfilMotoristaDbContext.cs new file mode 100644 index 00000000..54d392dc --- /dev/null +++ b/src/Motoristas.Core.Data.MongoDB/PerfilMotoristaDbContext.cs @@ -0,0 +1,11 @@ +using Motoristas.Core.States; + +namespace Motoristas.Core.Data.MongoDB +{ + public class PerfilMotoristaDbContext : MongoDbContext, IPerfilMotoristaDbContext + { + public PerfilMotoristaDbContext(string connectionString) : base(connectionString, Collections.PERFIS_MOTORISTA) + { + } + } +} \ No newline at end of file diff --git a/src/Motoristas.Core.Data.MongoDB/PerfilMotoristaRepository.cs b/src/Motoristas.Core.Data.MongoDB/PerfilMotoristaRepository.cs new file mode 100644 index 00000000..71a51825 --- /dev/null +++ b/src/Motoristas.Core.Data.MongoDB/PerfilMotoristaRepository.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MediatR; +using Motoristas.Core.States; + +namespace Motoristas.Core.Data.MongoDB +{ + public class PerfilMotoristaRepository : IPerfilMotoristaRepository + { + private readonly IPerfilMotoristaDbContext _dbContext; + private readonly IMediator _mediator; + + public PerfilMotoristaRepository(IPerfilMotoristaDbContext dbContext, IMediator mediator) + { + _dbContext = dbContext; + _mediator = mediator; + } + + public async Task Save(PerfilMotorista PerfilMotorista) + { + var state = PerfilMotorista.GetState() as PerfilMotoristaState; + await _dbContext.Save(state); + } + + public Task Load(string id) + { + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException(nameof(id)); + var state = _dbContext.Load(id); + if (state == null) throw new EntityNotFoundException(typeof(PerfilMotorista).Name, id); + return Task.FromResult(new PerfilMotorista(state)); + } + + public async Task Delete(string id) + { + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException(nameof(id)); + var state = _dbContext.Load(id); + if (state == null) throw new EntityNotFoundException(typeof(PerfilMotorista).Name, id); + _dbContext.Delete(id); + } + + public async Task> ListAll(IFiltroPerfilMotorista filtro) + { + var result = _dbContext.Find(filtro.Sort); + if (result == null) throw new InvalidOperationException(); + var coll = new List(); + result.ForEach(x => coll.Add(new PerfilMotorista(x))); + return Task.FromResult(coll).Result; + + } + } +} \ No newline at end of file diff --git a/src/Motoristas.Core/AssemblyVisibility.cs b/src/Motoristas.Core/AssemblyVisibility.cs new file mode 100644 index 00000000..55b376ff --- /dev/null +++ b/src/Motoristas.Core/AssemblyVisibility.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Perfil")] +[assembly: InternalsVisibleTo("Perfil.Handlers")] +[assembly: InternalsVisibleTo("Motoristas.Core.Data.MongoDB")] diff --git a/src/Motoristas.Core/Data/EntityNotFoundException.cs b/src/Motoristas.Core/Data/EntityNotFoundException.cs new file mode 100644 index 00000000..5a1fd04c --- /dev/null +++ b/src/Motoristas.Core/Data/EntityNotFoundException.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.Serialization; + +namespace Motoristas.Core.Data +{ + [Serializable] + public class EntityNotFoundException : Exception + { + public EntityNotFoundException(string entityName, string entityId) : base($"Entidade do tipo {entityName} com ID {entityId ?? "NULL"} não encontrada") + { + EntityName = entityName; + EntityId = entityId; + } + + protected EntityNotFoundException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } + + public string EntityName { get; } + public string EntityId { get; } + } +} \ No newline at end of file diff --git a/src/Motoristas.Core/Data/IFiltroPerfilMotorista.cs b/src/Motoristas.Core/Data/IFiltroPerfilMotorista.cs new file mode 100644 index 00000000..885a1dfc --- /dev/null +++ b/src/Motoristas.Core/Data/IFiltroPerfilMotorista.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Core.Data +{ + public interface IFiltroPerfilMotorista + { + string Sort { get; } + } +} diff --git a/src/Motoristas.Core/Data/IPerfilMotoristaRepository.cs b/src/Motoristas.Core/Data/IPerfilMotoristaRepository.cs new file mode 100644 index 00000000..39ea1d69 --- /dev/null +++ b/src/Motoristas.Core/Data/IPerfilMotoristaRepository.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Motoristas.Core.Data +{ + public interface IPerfilMotoristaRepository + { + Task Save(PerfilMotorista perfilMotorista); + Task Load(string id); + Task Delete(string id); + Task> ListAll(IFiltroPerfilMotorista filtro); + + } +} \ No newline at end of file diff --git a/src/Motoristas.Core/Endereco.cs b/src/Motoristas.Core/Endereco.cs new file mode 100644 index 00000000..ca73934a --- /dev/null +++ b/src/Motoristas.Core/Endereco.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace Motoristas.Core +{ + public class Endereco + { + + public Endereco(string logradouro, + string bairro, + string cidade, + string uf, + string cep, + string descricao, + string numero, + string complemento, + Coordenadas coordenadas) + { + Logradouro = logradouro; + Cidade = cidade; + Cep = cep ?? throw new ArgumentNullException(nameof(cep)); + Bairro = bairro; + Descricao = descricao; + Numero = numero; + Complemento = complemento; + Coordenadas = coordenadas; + Uf = uf; + } + + public string Logradouro { get; } + public string Bairro { get; } + public string Cidade { get; } + public string Uf { get; } + public string Cep { get; } + public string Descricao { get; } + public string Numero { get; } + public string Complemento { get; } + public Coordenadas Coordenadas { get; } + + public static bool MesmaCidade(Endereco a, Endereco b) + { + return a.Uf.Equals(b.Uf) + && a.Cidade.RemoveDiacritics().ToUpperInvariant().Equals(b.Cidade.RemoveDiacritics().ToUpperInvariant()); + } + } + + public class Coordenadas + { + public Coordenadas(double latitude, double longitude) + { + Latitude = latitude; + Longitude = longitude; + } + + public double Latitude { get; } + public double Longitude { get; } + } + + public static class RemoveDiacritcs + { + public static string RemoveDiacritics(this string input) + { + var normalizedString = input.Normalize(NormalizationForm.FormD); + var stringBuilder = new StringBuilder(); + + foreach (var c in normalizedString) + { + var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); + if (unicodeCategory != UnicodeCategory.NonSpacingMark) + stringBuilder.Append(c); + } + + return stringBuilder.ToString().Normalize(NormalizationForm.FormC); + } + } +} \ No newline at end of file diff --git a/src/Motoristas.Core/IAggregateRoot.cs b/src/Motoristas.Core/IAggregateRoot.cs new file mode 100644 index 00000000..d21c39dd --- /dev/null +++ b/src/Motoristas.Core/IAggregateRoot.cs @@ -0,0 +1,7 @@ +namespace Motoristas.Core +{ + public interface IAggregateRoot + { + object GetState(); + } +} diff --git a/src/Motoristas.Core/IEntity.cs b/src/Motoristas.Core/IEntity.cs new file mode 100644 index 00000000..b5443916 --- /dev/null +++ b/src/Motoristas.Core/IEntity.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using MediatR; + +namespace Motoristas.Core +{ + public interface IEntity + { + TId Id { get; } + } +} diff --git a/src/Motoristas.Core/IIdentityGenerator.cs b/src/Motoristas.Core/IIdentityGenerator.cs new file mode 100644 index 00000000..02c6d309 --- /dev/null +++ b/src/Motoristas.Core/IIdentityGenerator.cs @@ -0,0 +1,7 @@ +namespace Motoristas.Core +{ + public interface IIdentityGenerator + { + T Create(); + } +} diff --git a/src/Motoristas.Core/Mappers/StateMapperExtensions.cs b/src/Motoristas.Core/Mappers/StateMapperExtensions.cs new file mode 100644 index 00000000..5d789193 --- /dev/null +++ b/src/Motoristas.Core/Mappers/StateMapperExtensions.cs @@ -0,0 +1,78 @@ +using AutoMapper; +using AutoMapper.Mappers; +using Motoristas.Core.States; + +namespace Motoristas.Core.Mappers +{ + internal static class StateMapperExtensions + { + private static readonly IMapper Mapper; + + static StateMapperExtensions() + { + var config = new MapperConfiguration(c => + { + c.AddConditionalObjectMapper().Where((s, d) => s.Name == $"{d.Name}State"); + c.AddConditionalObjectMapper().Where((s, d) => $"{s.Name}State" == d.Name); + }); + config.AssertConfigurationIsValid(); + Mapper = config.CreateMapper(); + } + + #region Endereço + + public static Endereco ToDomain(this EnderecoState state) + { + return Mapper.Map(state); + } + + public static EnderecoState ToState(this Endereco model) + { + return Mapper.Map(model); + } + + #endregion + + #region Veiculo + public static Veiculo ToDomain(this VeiculoState state) + { + return Mapper.Map(state); + } + + public static VeiculoState ToState(this Veiculo model) + { + return Mapper.Map(model); + } + + #endregion + + #region Motorista + + public static Motorista ToDomain(this MotoristaState state) + { + return Mapper.Map(state); + } + + public static MotoristaState ToState(this Motorista model) + { + return Mapper.Map(model); + } + + #endregion + + #region Coordenadas + + public static Coordenadas ToDomain(this CoordenadasState state) + { + return Mapper.Map(state); + } + + public static CoordenadasState ToState(this Coordenadas model) + { + return Mapper.Map(model); + } + + #endregion + + } +} \ No newline at end of file diff --git a/src/Motoristas.Core/Motorista.cs b/src/Motoristas.Core/Motorista.cs new file mode 100644 index 00000000..a0ae778b --- /dev/null +++ b/src/Motoristas.Core/Motorista.cs @@ -0,0 +1,23 @@ +using System; + +namespace Motoristas.Core +{ + public class Motorista + { + public Motorista(string nome, + string ultimoNome, + Endereco endereco, + Veiculo veiculo) + { + Nome = nome ?? throw new ArgumentNullException(nameof(nome)); + UtimoNome = ultimoNome ?? throw new ArgumentNullException(nameof(ultimoNome)); + Endereco = endereco; + Veiculo = veiculo; + } + + public string Nome { get; } + public string UtimoNome { get; } + public Endereco Endereco { get; } + public Veiculo Veiculo { get; } + } +} \ No newline at end of file diff --git a/src/Motoristas.Core/Motoristas.Core.csproj b/src/Motoristas.Core/Motoristas.Core.csproj new file mode 100644 index 00000000..fef12db6 --- /dev/null +++ b/src/Motoristas.Core/Motoristas.Core.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/src/Motoristas.Core/PerfilMotorista.cs b/src/Motoristas.Core/PerfilMotorista.cs new file mode 100644 index 00000000..ceb7d131 --- /dev/null +++ b/src/Motoristas.Core/PerfilMotorista.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediatR; +using Motoristas.Core.Mappers; +using Motoristas.Core.States; + +namespace Motoristas.Core +{ + + public class PerfilMotorista : IAggregateRoot, IEntity + { + private readonly PerfilMotoristaState _state; + + internal PerfilMotorista(PerfilMotoristaState state) + { + _state = state; + } + + public PerfilMotorista(string id) + { + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id)); + + _state = new PerfilMotoristaState + { + Id = id, + DataCriacao = DateTimeOffset.Now + }; + + AtualizarDataModificacao(); + } + + #region Properties + + public string Id => _state.Id; + public DateTimeOffset DataCriacao => _state.DataCriacao; + public DateTimeOffset DataUltimaAtualizacao => _state.DataUltimaAtualizacao; + public Motorista Motorista => _state.Motorista.ToDomain(); + + #endregion + + private void AtualizarDataModificacao() + { + _state.DataUltimaAtualizacao = DateTimeOffset.Now; + } + + public void DefinirMotorista(Motorista motorista) + { + if (motorista == null) + throw new ArgumentNullException(nameof(motorista)); + + _state.Motorista = motorista.ToState(); + AtualizarDataModificacao(); + } + + public void DefinirCoordenadasEnderco(Coordenadas coordenadas) + { + if (coordenadas == null) + throw new ArgumentNullException(nameof(coordenadas)); + + _state.Motorista.Endereco.Coordenadas = coordenadas.ToState(); + } + + public object GetState() + { + return _state; + } + } +} \ No newline at end of file diff --git a/src/Motoristas.Core/Services/IGeolocationService.cs b/src/Motoristas.Core/Services/IGeolocationService.cs new file mode 100644 index 00000000..d4977236 --- /dev/null +++ b/src/Motoristas.Core/Services/IGeolocationService.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Core.Services +{ + public interface IGeolocationService + { + Coordenadas ObterCoordenadas(String endereco); + } +} diff --git a/src/Motoristas.Core/States/EnderecoState.cs b/src/Motoristas.Core/States/EnderecoState.cs new file mode 100644 index 00000000..8e80a56a --- /dev/null +++ b/src/Motoristas.Core/States/EnderecoState.cs @@ -0,0 +1,21 @@ +namespace Motoristas.Core.States +{ + public class EnderecoState + { + public string Descricao { get; set; } + public string Logradouro { get; set; } + public string Bairro { get; set; } + public string Cidade { get; set; } + public string Uf { get; set; } + public string Cep { get; set; } + public string Numero { get; set; } + public string Complemento { get; set; } + public CoordenadasState Coordenadas { get; set; } + } + + public class CoordenadasState + { + public double Latitude { get; set; } + public double Longitude { get; set; } + } +} diff --git a/src/Motoristas.Core/States/IStateIdentity.cs b/src/Motoristas.Core/States/IStateIdentity.cs new file mode 100644 index 00000000..3a6a96cb --- /dev/null +++ b/src/Motoristas.Core/States/IStateIdentity.cs @@ -0,0 +1,7 @@ +namespace Motoristas.Core.States +{ + public interface IStateIdentity + { + TId Id { get; } + } +} diff --git a/src/Motoristas.Core/States/MotoristaState.cs b/src/Motoristas.Core/States/MotoristaState.cs new file mode 100644 index 00000000..c89b770b --- /dev/null +++ b/src/Motoristas.Core/States/MotoristaState.cs @@ -0,0 +1,12 @@ +using System; + +namespace Motoristas.Core.States +{ + public class MotoristaState + { + public string Nome { get; set; } + public string UltimoNome { get; set; } + public EnderecoState Endereco { get; set; } + public VeiculoState Veiculo { get; set; } + } +} diff --git a/src/Motoristas.Core/States/PerfilMotoristaState.cs b/src/Motoristas.Core/States/PerfilMotoristaState.cs new file mode 100644 index 00000000..5dffbb49 --- /dev/null +++ b/src/Motoristas.Core/States/PerfilMotoristaState.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace Motoristas.Core.States +{ + public class PerfilMotoristaState : IStateIdentity + { + public string Id { get; set; } + public DateTimeOffset DataCriacao { get; set; } + public MotoristaState Motorista { get; set; } + public DateTimeOffset DataUltimaAtualizacao { get; set; } + } +} diff --git a/src/Motoristas.Core/States/VeiculoState.cs b/src/Motoristas.Core/States/VeiculoState.cs new file mode 100644 index 00000000..b2699fc2 --- /dev/null +++ b/src/Motoristas.Core/States/VeiculoState.cs @@ -0,0 +1,9 @@ +namespace Motoristas.Core.States +{ + public class VeiculoState + { + public string Marca { get; set; } + public string Modelo { get; set; } + public string Placa { get; set; } + } +} \ No newline at end of file diff --git a/src/Motoristas.Core/Veiculo.cs b/src/Motoristas.Core/Veiculo.cs new file mode 100644 index 00000000..b9ada7cd --- /dev/null +++ b/src/Motoristas.Core/Veiculo.cs @@ -0,0 +1,20 @@ +using System; + +namespace Motoristas.Core +{ + public class Veiculo + { + public Veiculo( string placa, + string marca, + string modelo) + { + Placa = placa ?? throw new ArgumentNullException(nameof(placa)); + Marca = marca ?? throw new ArgumentNullException(nameof(marca)); + Modelo = modelo ?? throw new ArgumentNullException(nameof(modelo)); + } + + public string Marca { get; } + public string Modelo { get; } + public string Placa { get; } + } +} \ No newline at end of file diff --git a/src/Motoristas.Handlers/Behaviors/ValidationBehavior.cs b/src/Motoristas.Handlers/Behaviors/ValidationBehavior.cs new file mode 100644 index 00000000..c5900ee2 --- /dev/null +++ b/src/Motoristas.Handlers/Behaviors/ValidationBehavior.cs @@ -0,0 +1,35 @@ +using FluentValidation; +using MediatR; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Motoristas.Handlers.Behaviors +{ + public class ValidationBehavior : IPipelineBehavior + where TRequest : IRequest + { + private readonly IEnumerable> _validators; + + public ValidationBehavior(IEnumerable> validators) + { + _validators = validators; + } + + public Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) + { + var context = new ValidationContext(request); + var failures = _validators + .Select(v => v.Validate(context)) + .SelectMany(result => result.Errors) + .Where(f => f != null) + .ToList(); + + if (failures.Count != 0) + throw new ValidationException(failures); + + return next(); + } + } +} diff --git a/src/Motoristas.Handlers/Commands/ObterPerfilMotorista.cs b/src/Motoristas.Handlers/Commands/ObterPerfilMotorista.cs new file mode 100644 index 00000000..b2d3eb45 --- /dev/null +++ b/src/Motoristas.Handlers/Commands/ObterPerfilMotorista.cs @@ -0,0 +1,17 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Handlers.Commands +{ + public class ObterPerfilMotorista : IRequest + { + public ObterPerfilMotorista(string perfilId) + { + PerfilId = perfilId; + } + + public string PerfilId { get; } + } +} diff --git a/src/Motoristas.Handlers/Commands/ObterPerfilMotoristaResponse.cs b/src/Motoristas.Handlers/Commands/ObterPerfilMotoristaResponse.cs new file mode 100644 index 00000000..fcc55e1d --- /dev/null +++ b/src/Motoristas.Handlers/Commands/ObterPerfilMotoristaResponse.cs @@ -0,0 +1,9 @@ +using Motoristas.Core; + +namespace Motoristas.Handlers.Commands +{ + public class ObterPerfilMotoristaResponse + { + public PerfilMotorista Perfil { get; set; } + } +} diff --git a/src/Motoristas.Handlers/Commands/PerfilMotoristaQuery.cs b/src/Motoristas.Handlers/Commands/PerfilMotoristaQuery.cs new file mode 100644 index 00000000..ceffbeb9 --- /dev/null +++ b/src/Motoristas.Handlers/Commands/PerfilMotoristaQuery.cs @@ -0,0 +1,13 @@ +using MediatR; +using Motoristas.Core.Data; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Handlers.Commands +{ + public class PerfilMotoristaQuery : IRequest, IFiltroPerfilMotorista + { + public string Sort { get; set; } + } +} diff --git a/src/Motoristas.Handlers/Commands/PerfilMotoristaQueryResponse.cs b/src/Motoristas.Handlers/Commands/PerfilMotoristaQueryResponse.cs new file mode 100644 index 00000000..4de62bbc --- /dev/null +++ b/src/Motoristas.Handlers/Commands/PerfilMotoristaQueryResponse.cs @@ -0,0 +1,12 @@ +using Motoristas.Core; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Handlers.Commands +{ + public class PerfilMotoristaQueryResponse + { + public List Lista { get; set; } + } +} diff --git a/src/Motoristas.Handlers/Commands/RegistrarPerfilMotorista.cs b/src/Motoristas.Handlers/Commands/RegistrarPerfilMotorista.cs new file mode 100644 index 00000000..179f634f --- /dev/null +++ b/src/Motoristas.Handlers/Commands/RegistrarPerfilMotorista.cs @@ -0,0 +1,18 @@ +using MediatR; +using Motoristas.Handlers.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Handlers.Commands +{ + public class RegistrarPerfilMotorista : IRequest + { + public RegistrarPerfilMotorista(MotoristaModel motorista) + { + Motorista = motorista; + } + + public MotoristaModel Motorista { get; } + } +} diff --git a/src/Motoristas.Handlers/Commands/RegistrarPerfilMotoristaResponse.cs b/src/Motoristas.Handlers/Commands/RegistrarPerfilMotoristaResponse.cs new file mode 100644 index 00000000..8dcba1b7 --- /dev/null +++ b/src/Motoristas.Handlers/Commands/RegistrarPerfilMotoristaResponse.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Handlers.Commands +{ + public class RegistrarPerfilMotoristaResponse + { + public string PerfilId { get; set; } + } +} diff --git a/src/Motoristas.Handlers/Commands/RemoverPerfilMotorista.cs b/src/Motoristas.Handlers/Commands/RemoverPerfilMotorista.cs new file mode 100644 index 00000000..bf2caa5c --- /dev/null +++ b/src/Motoristas.Handlers/Commands/RemoverPerfilMotorista.cs @@ -0,0 +1,17 @@ +using MediatR; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Handlers.Commands +{ + public class RemoverPerfilMotorista : IRequest + { + public RemoverPerfilMotorista(string perfilId) + { + PerfilId = perfilId; + } + + public string PerfilId { get; } + } +} diff --git a/src/Motoristas.Handlers/Commands/RemoverPerfilMotoristaResponse.cs b/src/Motoristas.Handlers/Commands/RemoverPerfilMotoristaResponse.cs new file mode 100644 index 00000000..9fd72b06 --- /dev/null +++ b/src/Motoristas.Handlers/Commands/RemoverPerfilMotoristaResponse.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Handlers.Commands +{ + public class RemoverPerfilMotoristaResponse + { + } +} diff --git a/src/Motoristas.Handlers/Mappers/ModelMapperExtensions.cs b/src/Motoristas.Handlers/Mappers/ModelMapperExtensions.cs new file mode 100644 index 00000000..1ec12b24 --- /dev/null +++ b/src/Motoristas.Handlers/Mappers/ModelMapperExtensions.cs @@ -0,0 +1,37 @@ +using AutoMapper; +using AutoMapper.Mappers; +using Motoristas.Core; +using Motoristas.Handlers.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Handlers.Mappers +{ + public static class ModelMapperExtensions + { + private static readonly IMapper Mapper; + + static ModelMapperExtensions() + { + var config = new MapperConfiguration(c => + { + c.AddConditionalObjectMapper().Where((s, d) => s.Name == $"{d.Name}Model"); + c.AddConditionalObjectMapper().Where((s, d) => $"{s.Name}Model" == d.Name); + }); + config.AssertConfigurationIsValid(); + Mapper = config.CreateMapper(); + } + + public static Motorista ToDomain(this MotoristaModel model) + { + return Mapper.Map(model); + } + + public static MotoristaModel ToModel(this Motorista domain) + { + return Mapper.Map(domain); + } + + } +} diff --git a/src/Motoristas.Handlers/Models/EnderecoModel.cs b/src/Motoristas.Handlers/Models/EnderecoModel.cs new file mode 100644 index 00000000..c041d9c6 --- /dev/null +++ b/src/Motoristas.Handlers/Models/EnderecoModel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Handlers.Models +{ + public class EnderecoModel + { + public string Descricao { get; set; } + public string Logradouro { get; set; } + public string Bairro { get; set; } + public string Cidade { get; set; } + public string Estado { get; set; } + public string Cep { get; set; } + public string Numero { get; set; } + public string Complemento { get; set; } + public CoordenadasModel Coordenadas { get; set; } + } + + public class CoordenadasModel + { + public double Latitude { get; set; } + public double Longitude { get; set; } + } +} diff --git a/src/Motoristas.Handlers/Models/MotoristaModel.cs b/src/Motoristas.Handlers/Models/MotoristaModel.cs new file mode 100644 index 00000000..56d63889 --- /dev/null +++ b/src/Motoristas.Handlers/Models/MotoristaModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Handlers.Models +{ + public class MotoristaModel + { + public string Nome { get; set; } + public string UltimoNome { get; set; } + public VeiculoModel Veiculo { get; set; } + public EnderecoModel Endereco { get; set; } + } +} diff --git a/src/Motoristas.Handlers/Models/VeiculoModel.cs b/src/Motoristas.Handlers/Models/VeiculoModel.cs new file mode 100644 index 00000000..64487526 --- /dev/null +++ b/src/Motoristas.Handlers/Models/VeiculoModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Motoristas.Handlers.Models +{ + public class VeiculoModel + { + public string Marca { get; set; } + public string Modelo { get; set; } + public string Placa { get; set; } + } +} diff --git a/src/Motoristas.Handlers/Motoristas.Handlers.csproj b/src/Motoristas.Handlers/Motoristas.Handlers.csproj new file mode 100644 index 00000000..f8fb8252 --- /dev/null +++ b/src/Motoristas.Handlers/Motoristas.Handlers.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp2.1 + + + + + + + + + + + + + diff --git a/src/Motoristas.Handlers/PerfilMotoristaRequestHandler.cs b/src/Motoristas.Handlers/PerfilMotoristaRequestHandler.cs new file mode 100644 index 00000000..63b5b94a --- /dev/null +++ b/src/Motoristas.Handlers/PerfilMotoristaRequestHandler.cs @@ -0,0 +1,67 @@ +using MediatR; +using Motoristas.Core; +using Motoristas.Core.Data; +using Motoristas.Core.Services; +using Motoristas.Handlers.Commands; +using Motoristas.Handlers.Mappers; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Motoristas.Handlers +{ + public class PerfilMotoristaRequestHandler : IRequestHandler, + IRequestHandler, + IRequestHandler, + IRequestHandler + { + private readonly IIdentityGenerator _identityGenerator; + + private readonly IPerfilMotoristaRepository _repository; + + private readonly IGeolocationService _geolocationService; + + public PerfilMotoristaRequestHandler(IIdentityGenerator identityGenerator, + IPerfilMotoristaRepository repository, + IGeolocationService geolocationService) + { + _identityGenerator = identityGenerator; + _repository = repository; + _geolocationService = geolocationService; + } + + public async Task Handle(RegistrarPerfilMotorista request, CancellationToken cancellationToken) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + var id = _identityGenerator.Create(); + var perfil = new PerfilMotorista(id); + perfil.DefinirMotorista(request.Motorista.ToDomain()); + perfil.DefinirCoordenadasEnderco(_geolocationService.ObterCoordenadas(request.Motorista.Endereco.Descricao)); + await _repository.Save(perfil); + var result = new RegistrarPerfilMotoristaResponse { PerfilId = id }; + return result; + } + + public async Task Handle(ObterPerfilMotorista request, CancellationToken cancellationToken) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + var perfil = await _repository.Load(request.PerfilId); + var result = new ObterPerfilMotoristaResponse { Perfil = perfil }; + return result; + } + + public async Task Handle(RemoverPerfilMotorista request, CancellationToken cancellationToken) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + await _repository.Delete(request.PerfilId); + var result = new RemoverPerfilMotoristaResponse(); + return result; + } + + public async Task Handle(PerfilMotoristaQuery request, CancellationToken cancellationToken) + { + var coll = _repository.ListAll(request).Result; + return new PerfilMotoristaQueryResponse { Lista = coll }; + } + } +} diff --git a/src/Motoristas.Handlers/Validators/CEPValidator.cs b/src/Motoristas.Handlers/Validators/CEPValidator.cs new file mode 100644 index 00000000..4b5ca60a --- /dev/null +++ b/src/Motoristas.Handlers/Validators/CEPValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using Motoristas.Helpers; + +namespace Motoristas.Handlers.Validators +{ + public class CEPValidator : AbstractValidator + { + public CEPValidator() + { + RuleFor(x => x) + .Must(x => CEPHelper.Validar(x)) + .WithMessage("Formato de CEP incorreto."); + } + } +} diff --git a/src/Motoristas.Handlers/Validators/EnderecoValidator.cs b/src/Motoristas.Handlers/Validators/EnderecoValidator.cs new file mode 100644 index 00000000..56ac38bb --- /dev/null +++ b/src/Motoristas.Handlers/Validators/EnderecoValidator.cs @@ -0,0 +1,18 @@ +using FluentValidation; +using Motoristas.Handlers.Models; + +namespace Motoristas.Handlers.Validators +{ + public class EnderecoValidator : AbstractValidator + { + public EnderecoValidator() + { + RuleFor(x => x.Cep) + .NotEmpty() + .WithMessage("CEP não definido."); + + RuleFor(x => x.Cep) + .SetValidator(new CEPValidator()); + } + } +} diff --git a/src/Motoristas.Handlers/Validators/MotoristaValidator.cs b/src/Motoristas.Handlers/Validators/MotoristaValidator.cs new file mode 100644 index 00000000..3cbe36bf --- /dev/null +++ b/src/Motoristas.Handlers/Validators/MotoristaValidator.cs @@ -0,0 +1,26 @@ +using FluentValidation; +using Motoristas.Handlers.Models; + +namespace Motoristas.Handlers.Validators +{ + public class MotoristaValidator : AbstractValidator + { + public MotoristaValidator() + { + RuleFor(x => x.Nome) + .NotEmpty() + .WithMessage("Nome não definido."); + + RuleFor(x => x.UltimoNome) + .NotEmpty() + .WithMessage("Nome não definido."); + + RuleFor(x => x.Endereco) + .NotNull() + .WithMessage("Endereco não preenchido."); + + RuleFor(x => x.Endereco) + .SetValidator(new EnderecoValidator()); + } + } +} diff --git a/src/Motoristas.Handlers/Validators/PlacaValidator.cs b/src/Motoristas.Handlers/Validators/PlacaValidator.cs new file mode 100644 index 00000000..0a944710 --- /dev/null +++ b/src/Motoristas.Handlers/Validators/PlacaValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using Motoristas.Helpers; + +namespace Motoristas.Handlers.Validators +{ + public class PlacaValidator : AbstractValidator + { + public PlacaValidator() + { + RuleFor(x => x) + .Must(x => PlacaHelper.Validar(x)) + .WithMessage("Placa com formato inválido."); + } + } +} \ No newline at end of file diff --git a/src/Motoristas.Handlers/Validators/VeiculoValidator.cs b/src/Motoristas.Handlers/Validators/VeiculoValidator.cs new file mode 100644 index 00000000..dc80020c --- /dev/null +++ b/src/Motoristas.Handlers/Validators/VeiculoValidator.cs @@ -0,0 +1,24 @@ +using FluentValidation; +using Motoristas.Handlers.Models; + +namespace Motoristas.Handlers.Validators +{ + public class VeiculoValidator : AbstractValidator + { + public VeiculoValidator() + { + RuleFor(x => x.Marca) + .NotEmpty() + .WithMessage("Marca não definida.") + .Length(2, 15) + .WithMessage("Marca com formato incorreto."); + + RuleFor(x => x.Modelo) + .NotEmpty() + .WithMessage("Modelo não definido."); + + RuleFor(x => x.Placa) + .SetValidator(new PlacaValidator()); + } + } +} diff --git a/src/Motoristas.Helpers/CEPHelper.cs b/src/Motoristas.Helpers/CEPHelper.cs new file mode 100644 index 00000000..12ed7a8a --- /dev/null +++ b/src/Motoristas.Helpers/CEPHelper.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace Motoristas.Helpers +{ + public static class CEPHelper + { + public static bool Validar(string cep) + { + if (string.IsNullOrWhiteSpace(cep)) + return false; + + return Regex.IsMatch(cep, @"\d{5}[-]{0,1}\d{3}"); + } + } +} diff --git a/src/Motoristas.Helpers/Motoristas.Helpers.csproj b/src/Motoristas.Helpers/Motoristas.Helpers.csproj new file mode 100644 index 00000000..86ea3bbe --- /dev/null +++ b/src/Motoristas.Helpers/Motoristas.Helpers.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp2.1 + + + diff --git a/src/Motoristas.Helpers/PlacaHelper.cs b/src/Motoristas.Helpers/PlacaHelper.cs new file mode 100644 index 00000000..2a0d6631 --- /dev/null +++ b/src/Motoristas.Helpers/PlacaHelper.cs @@ -0,0 +1,15 @@ +using System.Text.RegularExpressions; + +namespace Motoristas.Helpers +{ + public static class PlacaHelper + { + public static bool Validar(string placa) + { + if (string.IsNullOrWhiteSpace(placa)) + return false; + + return Regex.IsMatch(placa, @"^[A-Z]{3}\d[\d|A-J]\d{2}$", RegexOptions.IgnoreCase); + } + } +} diff --git a/src/Motoristas/Bootstrapper.cs b/src/Motoristas/Bootstrapper.cs new file mode 100644 index 00000000..62e816d2 --- /dev/null +++ b/src/Motoristas/Bootstrapper.cs @@ -0,0 +1,74 @@ +using Flurl.Http.Configuration; +using GoogleGeolocationService; +using MediatR; +using MicroservicesPlatform.Configuration; +using Microsoft.Extensions.Configuration; +using Motoristas.Config; +using Motoristas.Core; +using Motoristas.Core.Data; +using Motoristas.Core.Data.MongoDB; +using Motoristas.Core.Services; +using Motoristas.Handlers; +using Motoristas.Handlers.Commands; +using Nancy; +using Nancy.Bootstrapper; +using Nancy.TinyIoc; +using Serilog; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Motoristas +{ + public class Bootstrapper : DefaultNancyBootstrapper + { + private readonly ILogger _log; + private readonly IConfiguration _configuration; + + public Bootstrapper(ILogger log, IConfiguration configuration) + { + _log = log ?? throw new ArgumentNullException(nameof(log)); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } + + protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) + { + pipelines.OnError += (ctx, ex) => + { + _log.Error(ex, "Unhandled exception"); + return null; + }; + } + + protected override void ConfigureApplicationContainer(TinyIoCContainer container) + { + base.ConfigureApplicationContainer(container); + var dbConfig = _configuration.GetConfig("Database"); + var googleServiceConfig = _configuration.GetConfig("GoogleGeoService"); + + container.Register(_log); + container.Register(new Mediator(container.Resolve)); + container.Register(); + container.Register(new PerfilMotoristaDbContext(dbConfig.ConnectionString)); + container.Register(); + container.Register, MongoDbIdentityGenerator>(); + + container.Register(new GeolocationService(new GoogleClientConfig(googleServiceConfig.Address, googleServiceConfig.Key), _log)); + + // TinyIoC não suporta open generics... + container.Register(typeof(IRequestHandler<,>)); + container.Register, PerfilMotoristaRequestHandler>(); + container.Register, PerfilMotoristaRequestHandler>(); + container.Register, PerfilMotoristaRequestHandler>(); + container.Register, PerfilMotoristaRequestHandler>(); + } + + protected override void ConfigureRequestContainer(TinyIoCContainer container, NancyContext context) + { + base.ConfigureRequestContainer(container, context); + + var httpClientFactory = container.Resolve(); + } + } +} diff --git a/src/Motoristas/Config/GeolocationServiceConfig.cs b/src/Motoristas/Config/GeolocationServiceConfig.cs new file mode 100644 index 00000000..aad99d47 --- /dev/null +++ b/src/Motoristas/Config/GeolocationServiceConfig.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Motoristas.Config +{ + public class GeolocationServiceConfig + { + public string Address { get; set; } + public string Key { get; set; } + } +} diff --git a/src/Motoristas/ConfigurationExtensions.cs b/src/Motoristas/ConfigurationExtensions.cs new file mode 100644 index 00000000..0872ecd0 --- /dev/null +++ b/src/Motoristas/ConfigurationExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Motoristas +{ + public static class ConfigurationExtensions + { + public static TOptions GetConfig(this IConfiguration config, string sectionName) + where TOptions : class, new() + { + if (string.IsNullOrWhiteSpace(sectionName)) throw new ArgumentException(nameof(sectionName)); + var options = new TOptions(); + config.GetSection(sectionName).Bind(options); + return options; + } + } +} diff --git a/src/Motoristas/Modules/MonitorModule.cs b/src/Motoristas/Modules/MonitorModule.cs new file mode 100644 index 00000000..ab937e89 --- /dev/null +++ b/src/Motoristas/Modules/MonitorModule.cs @@ -0,0 +1,16 @@ +using Nancy; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Motoristas.Modules +{ + public class MonitorModule : NancyModule + { + public MonitorModule() : base("/_monitor") + { + Get("/shallow", _ => Negotiate.WithStatusCode(HttpStatusCode.OK)); + } + } +} diff --git a/src/Motoristas/Modules/MotoristasModule.cs b/src/Motoristas/Modules/MotoristasModule.cs new file mode 100644 index 00000000..6752a282 --- /dev/null +++ b/src/Motoristas/Modules/MotoristasModule.cs @@ -0,0 +1,115 @@ +using MediatR; +using Motoristas.Handlers.Commands; +using Nancy; +using Nancy.ModelBinding; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Motoristas.Modules +{ + public static class BasePathMapping + { + static BasePathMapping() + { + BasePath = string.Empty; + } + + public static string BasePath { get; set; } + } + + public class MotoristasModule : NancyModule + { + private readonly IMediator _mediator; + private readonly string _basePathMapping = BasePathMapping.BasePath; + + public MotoristasModule(IMediator mediator) : base("/motoristas") + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + + //TODO: Implement Athentication ! + //this.RequiresAuthentication(); + + Post("", async p => + { + var cmd = this.Bind(); + + var response = await mediator.Send(cmd); + var result = CreatePerfilResponse(response.PerfilId); + return Negotiate.WithStatusCode(HttpStatusCode.Created) + .WithHeader("Location", Flurl.Url.Combine(BasePathMapping.BasePath, ModulePath, response.PerfilId)) + .WithModel(result); + }); + + Put("/{perfilId}/remover-cadastro", async p => + { + var cmd = new RemoverPerfilMotorista(p.perfilId); + await mediator.Send(cmd); + return Negotiate.WithStatusCode(HttpStatusCode.SeeOther) + .WithHeader("Location", Flurl.Url.Combine(BasePathMapping.BasePath, ModulePath, cmd.PerfilId)); + }); + + Get("/{perfilId}", async p => + { + var cmd = new ObterPerfilMotorista(p.perfilId); + await _mediator.Send(cmd); + var result = CreatePerfilResponse(cmd.PerfilId); + return Negotiate.WithStatusCode(HttpStatusCode.OK) + .WithModel(result); + }); + + Get(_basePathMapping, async p => + { + var cmd = new PerfilMotoristaQuery { Sort = Request.Query["sort"] }; + var response = await _mediator.Send(cmd); + var result = new + { + Href = $"{Request.Path}", + Veiculos = response.Lista + .Select(v => new + { + v.Id, + v.DataCriacao, + v.DataUltimaAtualizacao, + NomeMotorista = v.Motorista.Nome, + UltimoNomeMotorista = v.Motorista.UtimoNome, + VeiculoMarca = v.Motorista.Veiculo.Marca, + VeiculoModelo = v.Motorista.Veiculo.Modelo, + VeiculoPlaca = v.Motorista.Veiculo.Placa, + EnderecoLogradouro = v.Motorista.Endereco.Logradouro, + EnderecoBairro = v.Motorista.Endereco.Bairro, + EnderecoCidade = v.Motorista.Endereco.Cidade, + EnderecoUf = v.Motorista.Endereco.Uf, + EnderecoCep = v.Motorista.Endereco.Cep, + EnderecoDescricao = v.Motorista.Endereco.Descricao, + EnderecoNumero = v.Motorista.Endereco.Numero, + EnderecoComplemento = v.Motorista.Endereco.Complemento, + Links = new[] + { + new + { + Relative = "perfil", + Href = $"{_basePathMapping}{ModulePath}/{v.Id}" + } + } + }) + }; + return Negotiate.WithStatusCode(HttpStatusCode.OK) + .WithModel(result); + }); + + } + + + private object CreatePerfilResponse(string perfilId) + { + var result = new + { + Href = Request.Path, + PerfilId = perfilId, + }; + return result; + } + } +} diff --git a/src/Motoristas/Motoristas.csproj b/src/Motoristas/Motoristas.csproj new file mode 100644 index 00000000..414a02a4 --- /dev/null +++ b/src/Motoristas/Motoristas.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp2.1 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Motoristas/Program.cs b/src/Motoristas/Program.cs new file mode 100644 index 00000000..ea6b2dd7 --- /dev/null +++ b/src/Motoristas/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Motoristas +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/src/Motoristas/Startup.cs b/src/Motoristas/Startup.cs new file mode 100644 index 00000000..d75f7b51 --- /dev/null +++ b/src/Motoristas/Startup.cs @@ -0,0 +1,101 @@ +using System.Globalization; +using MicroservicesPlatform.Configuration; +using MicroservicesPlatform.Middlewares; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Motoristas.Modules; +using Nancy.Owin; +using Serilog; +using Serilog.Events; +using Serilog.Formatting; +using Serilog.Formatting.Display; +using Serilog.Formatting.Elasticsearch; +using Serilog.Sinks.Elasticsearch; + +namespace Motoristas +{ + public class Startup + { + private readonly IHostingEnvironment _env; + private readonly IConfiguration _configuration; + + public Startup(IHostingEnvironment env, IConfiguration configuration) + { + _env = env; + _configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + if (!_env.IsDevelopment()) + { + BasePathMapping.BasePath = "/motoristas"; + } + + + void ConfigureAuthenticationOptions(AuthenticationOptions options) + { + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + } + + void ConfigureJwtBearerOptions(JwtBearerOptions options) + { + var authConfig = new AuthenticationConfig(); + _configuration.GetSection("Auth").Bind(authConfig); + options.Authority = authConfig.Authority; + options.Audience = authConfig.Audience; + options.RequireHttpsMetadata = true; + } + + services.AddCors() + .AddAuthentication(ConfigureAuthenticationOptions) + .AddJwtBearer(ConfigureJwtBearerOptions); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + var corsConfig = new CORSConfig(); + _configuration.GetSection("CORS").Bind(corsConfig); + + void ConfigureCorsPolicy(CorsPolicyBuilder builder) => builder.WithOrigins(corsConfig.AllowOrigin) + .AllowCredentials() + .AllowAnyMethod() + .AllowAnyHeader(); + + var logLevel = _env.IsDevelopment() + ? LogEventLevel.Debug + : LogEventLevel.Information; + + var formatter = _env.IsDevelopment() + ? (ITextFormatter)new MessageTemplateTextFormatter("{NewLine}{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3} {CorrelationToken} {Message}{NewLine}{Exception}", CultureInfo.InvariantCulture) + : new ElasticsearchJsonFormatter(); + + var log = LoggerFactory.CreateLogger(logLevel, formatter); + + app.UseCors(ConfigureCorsPolicy) + .UseOwin() + .UseLogging(log) + .UseNancy(o => o.Bootstrapper = new Bootstrapper(log, _configuration)); + } + } + public static class LoggerFactory + { + public static ILogger CreateLogger(LogEventLevel logLevel, ITextFormatter formatter) + { + return new LoggerConfiguration() + .MinimumLevel.Is(logLevel) + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.Console(formatter) + .CreateLogger(); + } + } +} diff --git a/src/Motoristas/appsettings.Development.json b/src/Motoristas/appsettings.Development.json new file mode 100644 index 00000000..e203e940 --- /dev/null +++ b/src/Motoristas/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Motoristas/appsettings.json b/src/Motoristas/appsettings.json new file mode 100644 index 00000000..69ccf204 --- /dev/null +++ b/src/Motoristas/appsettings.json @@ -0,0 +1,8 @@ +{ + "Database": { + "ConnectionString": "http://localhost:9200/" + }, + "CORS": { + "AllowOrigin": "*" + } +} diff --git a/tests/Motoristas.Tests/GoogleGeocodingTest.cs b/tests/Motoristas.Tests/GoogleGeocodingTest.cs new file mode 100644 index 00000000..aa80a586 --- /dev/null +++ b/tests/Motoristas.Tests/GoogleGeocodingTest.cs @@ -0,0 +1,28 @@ +using GoogleGeolocationService; +using Motoristas.Config; +using Motoristas.Core.Services; +using NSubstitute; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; +using Serilog; + +namespace Motoristas.Tests +{ + public class GoogleGeocodingTest + { + [Fact] + public async void Can_handle_google_geolocation_request() + { + var config = new GoogleClientConfig("https://maps.googleapis.com/maps/api/geocode/json", "z53osl/q"); + var log = Substitute.For(); + IGeolocationService service = new GeolocationService(config, log); + + var result = service.ObterCoordenadas("Avenida Paulista, 1009"); + + Assert.NotNull(result); + + } + } +} diff --git a/tests/Motoristas.Tests/Motoristas.Tests.csproj b/tests/Motoristas.Tests/Motoristas.Tests.csproj new file mode 100644 index 00000000..6962f582 --- /dev/null +++ b/tests/Motoristas.Tests/Motoristas.Tests.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp2.1 + + false + + + + + + + + + + + + + + + + + + diff --git a/tests/Motoristas.Tests/PerfilMotoristaRequestHandlerTests.cs b/tests/Motoristas.Tests/PerfilMotoristaRequestHandlerTests.cs new file mode 100644 index 00000000..d4dd1def --- /dev/null +++ b/tests/Motoristas.Tests/PerfilMotoristaRequestHandlerTests.cs @@ -0,0 +1,107 @@ +using Motoristas.Core; +using Motoristas.Core.Data; +using Motoristas.Core.Services; +using Motoristas.Handlers; +using Motoristas.Handlers.Commands; +using NSubstitute; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using Xunit; + +namespace Motoristas.Tests +{ + public class PerfilMotoristaRequestHandlerTests + { + + + [Fact] + public async void Can_handle_registrar_perfil_request() + { + var service = Substitute.For(); + var repository = Substitute.For(); + var identityGenerator = Substitute.For>(); + + var handler = new PerfilMotoristaRequestHandler(identityGenerator, repository, service); + + var request = new RegistrarPerfilMotorista(new Handlers.Models.MotoristaModel + { + Nome = "PrimeiroNome", + UltimoNome = "Sobrenome", + Veiculo = new Handlers.Models.VeiculoModel + { + Marca = "MarcaCarro", + Modelo = "ModeloCarro", + Placa = "PLA-0123" + }, + Endereco = new Handlers.Models.EnderecoModel + { + Bairro = "Bairro Teste", + Cep = "09812-180", + Cidade = "Cidade Teste", + Complemento = "Complemento Teste", + Descricao = "Avenida Paulista, 1009", + Estado = "SP", + Logradouro = "Avenida Paulista, 1009", + Numero = "1009A" + } + }); + + var result = await handler.Handle(request, CancellationToken.None); + + Assert.NotNull(result); + Assert.NotEmpty(result.PerfilId); + } + + [Fact] + public async void Can_handle_obter_perfil_request() + { + var service = Substitute.For(); + var repository = Substitute.For(); + var identityGenerator = Substitute.For>(); + + var handler = new PerfilMotoristaRequestHandler(identityGenerator, repository, service); + + var request = new ObterPerfilMotorista("1234"); + + var result = await handler.Handle(request, CancellationToken.None); + + Assert.NotNull(result); + } + + [Fact] + public async void Can_handle_remover_perfil_request() + { + var service = Substitute.For(); + var repository = Substitute.For(); + var identityGenerator = Substitute.For>(); + + var handler = new PerfilMotoristaRequestHandler(identityGenerator, repository, service); + + var request = new RemoverPerfilMotorista("1234"); + + await Assert.ThrowsAsync(() => handler.Handle(request, CancellationToken.None)); + + } + + [Fact] + public async void Can_handle_listar_perfil_request() + { + var service = Substitute.For(); + var repository = Substitute.For(); + var identityGenerator = Substitute.For>(); + + var handler = new PerfilMotoristaRequestHandler(identityGenerator, repository, service); + + var request = new PerfilMotoristaQuery(); + request.Sort = "Nome"; + + var result = await handler.Handle(request, CancellationToken.None); + + Assert.NotNull(result.Lista); + Assert.NotEmpty(result.Lista); + } + } +} +