@ -0,0 +1,21 @@ | |||
using MediatR; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Runtime.Serialization; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.Commands | |||
{ | |||
public class CancelOrderCommand : IAsyncRequest<bool> | |||
{ | |||
[DataMember] | |||
public int OrderNumber { get; private set; } | |||
public CancelOrderCommand(int orderNumber) | |||
{ | |||
OrderNumber = orderNumber; | |||
} | |||
} | |||
} |
@ -0,0 +1,15 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.IntegrationCommands.Commands | |||
{ | |||
public class ConfirmGracePeriodCommandMsg : IntegrationEvent | |||
{ | |||
public int OrderNumber { get; private set; } | |||
//TODO: message should change to Integration command type once command bus is implemented | |||
} | |||
} |
@ -0,0 +1,14 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
namespace Ordering.API.Application.IntegrationCommands.Commands | |||
{ | |||
public class SubmitOrderCommandMsg : IntegrationEvent | |||
{ | |||
public int OrderNumber { get; private set; } | |||
//TODO: message should change to Integration command type once command bus is implemented | |||
} | |||
} |
@ -1,10 +1,7 @@ | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.IntegrationEvents | |||
namespace Ordering.API.Application.IntegrationEvents | |||
{ | |||
public interface IOrderingIntegrationEventService | |||
{ |
@ -0,0 +1,126 @@ | |||
using Autofac.Features.OwnedInstances; | |||
using MediatR; | |||
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; | |||
using Ordering.API.Application.Commands; | |||
using Ordering.API.Application.IntegrationCommands.Commands; | |||
using Ordering.Domain.Exceptions; | |||
using System; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.Sagas | |||
{ | |||
/// <summary> | |||
/// Saga for handling the place order process | |||
/// and which is started once the basket.api has | |||
/// successfully processed the items ordered. | |||
/// Saga provides a period of grace to give the customer | |||
/// the opportunity to cancel the order before proceeding | |||
/// with the validations. | |||
/// </summary> | |||
public class OrderProcessSaga : Saga<Order>, | |||
IIntegrationEventHandler<SubmitOrderCommandMsg>, | |||
IIntegrationEventHandler<ConfirmGracePeriodCommandMsg>, | |||
IAsyncRequestHandler<CancelOrderCommand, bool> | |||
{ | |||
private readonly IMediator _mediator; | |||
private readonly Func<Owned<OrderingContext>> _dbContextFactory; | |||
public OrderProcessSaga( | |||
Func<Owned<OrderingContext>> dbContextFactory, OrderingContext orderingContext, | |||
IMediator mediator) | |||
: base(orderingContext) | |||
{ | |||
_dbContextFactory = dbContextFactory; | |||
_mediator = mediator; | |||
} | |||
/// <summary> | |||
/// Command handler which starts the create order process | |||
/// and initializes the saga | |||
/// </summary> | |||
/// <param name="command"> | |||
/// Integration command message which is sent by the | |||
/// basket.api once it has successfully process the | |||
/// order items. | |||
/// </param> | |||
/// <returns></returns> | |||
public async Task Handle(SubmitOrderCommandMsg command) | |||
{ | |||
var orderSaga = FindSagaById(command.OrderNumber); | |||
CheckValidSagaId(orderSaga); | |||
// TODO: This handler should change to Integration command handler type once command bus is implemented | |||
// TODO: Send createOrder Command | |||
// TODO: Set saga timeout | |||
} | |||
/// <summary> | |||
/// Command handler which confirms that the grace period | |||
/// has been completed and order has not been cancelled. | |||
/// If so, the process continues for validation. | |||
/// </summary> | |||
/// <param name="command"> | |||
/// Integration command message which is sent by a saga | |||
/// scheduler which provides the sagas that its grace | |||
/// period has completed. | |||
/// </param> | |||
/// <returns></returns> | |||
public async Task Handle(ConfirmGracePeriodCommandMsg command) | |||
{ | |||
var orderSaga = FindSagaById(command.OrderNumber); | |||
CheckValidSagaId(orderSaga); | |||
// TODO: This handler should change to Integration command handler type once command bus is implemented | |||
// TODO: If order status is not cancelled, change state to awaitingValidation and | |||
// send ConfirmOrderStockCommandMsg to Inventory api | |||
} | |||
/// <summary> | |||
/// Handler which processes the command when | |||
/// customer executes cancel order from app | |||
/// </summary> | |||
/// <param name="command"></param> | |||
/// <returns></returns> | |||
public async Task<bool> Handle(CancelOrderCommand command) | |||
{ | |||
var orderSaga = FindSagaById(command.OrderNumber); | |||
CheckValidSagaId(orderSaga); | |||
// Set order status tu cancelled | |||
return true; | |||
} | |||
private void CheckValidSagaId(Order orderSaga) | |||
{ | |||
if (orderSaga is null) | |||
{ | |||
throw new OrderingDomainException("Not able to process order saga event. Reason: no valid orderId"); | |||
} | |||
} | |||
#region CommandHandlerIdentifiers | |||
public class CancelOrderCommandIdentifiedHandler : IdentifierCommandHandler<CancelOrderCommand, bool> | |||
{ | |||
public CancelOrderCommandIdentifiedHandler(IMediator mediator, IRequestManager requestManager) : base(mediator, requestManager) | |||
{ | |||
} | |||
protected override bool CreateResultForDuplicateRequest() | |||
{ | |||
return true; // Ignore duplicate requests for processing order. | |||
} | |||
} | |||
#endregion | |||
} | |||
} |
@ -0,0 +1,30 @@ | |||
using Microsoft.EntityFrameworkCore; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
namespace Ordering.API.Application.Sagas | |||
{ | |||
public abstract class Saga<TEntity> where TEntity : Entity | |||
{ | |||
private readonly DbContext _dbContext; | |||
public Saga(DbContext dbContext) | |||
{ | |||
_dbContext = dbContext; | |||
} | |||
protected TEntity FindSagaById(int id, DbContext context = null) | |||
{ | |||
var ctx = context ?? _dbContext; | |||
return ctx.Set<TEntity>().Where(x => x.Id == id).SingleOrDefault(); | |||
} | |||
protected async Task<bool> SaveChangesAsync(DbContext context = null) | |||
{ | |||
var ctx = context ?? _dbContext; | |||
var result = await ctx.SaveChangesAsync(); | |||
return result > 0; | |||
} | |||
} | |||
} |
@ -0,0 +1,3 @@ | |||
* | |||
!obj/Docker/publish/* | |||
!obj/Docker/empty/ |
@ -0,0 +1,19 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using Microsoft.AspNetCore.Mvc; | |||
// For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 | |||
namespace Payment.API.Controllers | |||
{ | |||
public class HomeController : Controller | |||
{ | |||
// GET: /<controller>/ | |||
public IActionResult Index() | |||
{ | |||
return new RedirectResult("~/swagger/ui"); | |||
} | |||
} | |||
} |
@ -0,0 +1,46 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using Microsoft.AspNetCore.Mvc; | |||
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 | |||
namespace Payment.API.Controllers | |||
{ | |||
[Route("api/v1/[controller]")] | |||
public class PaymentController : Controller | |||
{ | |||
// GET: api/values | |||
[HttpGet] | |||
public IEnumerable<string> Get() | |||
{ | |||
return new string[] { "value1", "value2" }; | |||
} | |||
// GET api/values/5 | |||
[HttpGet("{id}")] | |||
public string Get(int id) | |||
{ | |||
return "value"; | |||
} | |||
// POST api/values | |||
[HttpPost] | |||
public void Post([FromBody]string value) | |||
{ | |||
} | |||
// PUT api/values/5 | |||
[HttpPut("{id}")] | |||
public void Put(int id, [FromBody]string value) | |||
{ | |||
} | |||
// DELETE api/values/5 | |||
[HttpDelete("{id}")] | |||
public void Delete(int id) | |||
{ | |||
} | |||
} | |||
} |
@ -0,0 +1,6 @@ | |||
FROM microsoft/aspnetcore:1.1 | |||
ARG source | |||
WORKDIR /app | |||
EXPOSE 80 | |||
COPY ${source:-obj/Docker/publish} . | |||
ENTRYPOINT ["dotnet", "Payment.API.dll"] |
@ -0,0 +1,28 @@ | |||
<Project Sdk="Microsoft.NET.Sdk.Web"> | |||
<PropertyGroup> | |||
<TargetFramework>netcoreapp1.1</TargetFramework> | |||
<DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath> | |||
<PackageTargetFallback>portable-net45+win8</PackageTargetFallback> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<Folder Include="wwwroot\" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.0.0" /> | |||
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" /> | |||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" /> | |||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.1" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.1" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="1.1.1" /> | |||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.Design" Version="1.1.1" /> | |||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" /> | |||
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="1.1.0" /> | |||
<PackageReference Include="Swashbuckle" Version="6.0.0-beta902" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.0" /> | |||
</ItemGroup> | |||
</Project> |
@ -0,0 +1,25 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.IO; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using Microsoft.AspNetCore.Builder; | |||
using Microsoft.AspNetCore.Hosting; | |||
namespace Payment.API | |||
{ | |||
public class Program | |||
{ | |||
public static void Main(string[] args) | |||
{ | |||
var host = new WebHostBuilder() | |||
.UseKestrel() | |||
.UseContentRoot(Directory.GetCurrentDirectory()) | |||
.UseStartup<Startup>() | |||
.UseApplicationInsights() | |||
.Build(); | |||
host.Run(); | |||
} | |||
} | |||
} |
@ -0,0 +1,29 @@ | |||
{ | |||
"iisSettings": { | |||
"windowsAuthentication": false, | |||
"anonymousAuthentication": true, | |||
"iisExpress": { | |||
"applicationUrl": "http://localhost:3330/", | |||
"sslPort": 0 | |||
} | |||
}, | |||
"profiles": { | |||
"IIS Express": { | |||
"commandName": "IISExpress", | |||
"launchBrowser": true, | |||
"launchUrl": "api/values", | |||
"environmentVariables": { | |||
"ASPNETCORE_ENVIRONMENT": "Development" | |||
} | |||
}, | |||
"Payment.API": { | |||
"commandName": "Project", | |||
"launchBrowser": true, | |||
"launchUrl": "api/values", | |||
"environmentVariables": { | |||
"ASPNETCORE_ENVIRONMENT": "Development" | |||
}, | |||
"applicationUrl": "http://localhost:3331" | |||
} | |||
} | |||
} |
@ -0,0 +1,55 @@ | |||
using Microsoft.AspNetCore.Builder; | |||
using Microsoft.AspNetCore.Hosting; | |||
using Microsoft.Extensions.Configuration; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Microsoft.Extensions.Logging; | |||
namespace Payment.API | |||
{ | |||
public class Startup | |||
{ | |||
public Startup(IHostingEnvironment env) | |||
{ | |||
var builder = new ConfigurationBuilder() | |||
.SetBasePath(env.ContentRootPath) | |||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) | |||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) | |||
.AddEnvironmentVariables(); | |||
Configuration = builder.Build(); | |||
} | |||
public IConfigurationRoot Configuration { get; } | |||
// This method gets called by the runtime. Use this method to add services to the container. | |||
public void ConfigureServices(IServiceCollection services) | |||
{ | |||
// Add framework services. | |||
services.AddMvc(); | |||
services.AddSwaggerGen(); | |||
services.ConfigureSwaggerGen(options => | |||
{ | |||
options.DescribeAllEnumsAsStrings(); | |||
options.SingleApiVersion(new Swashbuckle.Swagger.Model.Info() | |||
{ | |||
Title = "eShopOnContainers - Payment HTTP API", | |||
Version = "v1", | |||
Description = "The Payment Microservice HTTP API. This is a Data-Driven/CRUD microservice sample", | |||
TermsOfService = "Terms Of Service" | |||
}); | |||
}); | |||
} | |||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |||
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) | |||
{ | |||
loggerFactory.AddConsole(Configuration.GetSection("Logging")); | |||
loggerFactory.AddDebug(); | |||
app.UseMvcWithDefaultRoute(); | |||
app.UseSwagger() | |||
.UseSwaggerUi(); | |||
} | |||
} | |||
} |
@ -0,0 +1,10 @@ | |||
{ | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Debug", | |||
"System": "Information", | |||
"Microsoft": "Information" | |||
} | |||
} | |||
} |
@ -0,0 +1,8 @@ | |||
{ | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Warning" | |||
} | |||
} | |||
} |