Browse Source

add missed coupon API

pull/2021/head
Zhuk, Anton 2 years ago
parent
commit
0abcc5b688
28 changed files with 1043 additions and 2 deletions
  1. +28
    -0
      src/Services/Coupon/Coupon.API/Controllers/CouponController.cs
  2. +40
    -0
      src/Services/Coupon/Coupon.API/Coupon.API.csproj
  3. +15
    -0
      src/Services/Coupon/Coupon.API/CouponSettings.cs
  4. +25
    -0
      src/Services/Coupon/Coupon.API/Dockerfile
  5. +9
    -0
      src/Services/Coupon/Coupon.API/Dtos/CouponDto.cs
  6. +7
    -0
      src/Services/Coupon/Coupon.API/Dtos/IMapper.cs
  7. +16
    -0
      src/Services/Coupon/Coupon.API/Dtos/Mapper.cs
  8. +49
    -0
      src/Services/Coupon/Coupon.API/Extensions/IHostBuilderExtensions.cs
  9. +227
    -0
      src/Services/Coupon/Coupon.API/Extensions/IServiceCollectionExtensions.cs
  10. +35
    -0
      src/Services/Coupon/Coupon.API/Filters/AuthorizeCheckOperationFilter.cs
  11. +16
    -0
      src/Services/Coupon/Coupon.API/Filters/ValidateModelAttribute.cs
  12. +52
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/CouponSeed.cs
  13. +20
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/Models/Coupon.cs
  14. +25
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/Repositories/CouponContext.cs
  15. +42
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/Repositories/CouponRepository.cs
  16. +14
    -0
      src/Services/Coupon/Coupon.API/Infrastructure/Repositories/ICouponRepository.cs
  17. +56
    -0
      src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandlers/OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler.cs
  18. +22
    -0
      src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandlers/OrderStatusChangedToCancelledIntegrationEventHandler.cs
  19. +17
    -0
      src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs
  20. +17
    -0
      src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs
  21. +20
    -0
      src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs
  22. +24
    -0
      src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToCancelledIntegrationEvent.cs
  23. +53
    -0
      src/Services/Coupon/Coupon.API/Program.cs
  24. +27
    -0
      src/Services/Coupon/Coupon.API/Properties/launchSettings.json
  25. +85
    -0
      src/Services/Coupon/Coupon.API/Startup.cs
  26. +18
    -0
      src/Services/Coupon/Coupon.API/appsettings.Development.json
  27. +28
    -0
      src/Services/Coupon/Coupon.API/appsettings.json
  28. +56
    -2
      src/eShopOnContainers-ServicesAndWebApps.sln

+ 28
- 0
src/Services/Coupon/Coupon.API/Controllers/CouponController.cs View File

@ -0,0 +1,28 @@
namespace Coupon.API.Controllers
{
using System.Net;
using System.Threading.Tasks;
using Coupon.API.Dtos;
using Coupon.API.Infrastructure.Models;
using Coupon.API.Infrastructure.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
[Authorize]
[ApiController]
[Route("api/v1/[controller]")]
public class CouponController : ControllerBase
{
private readonly ICouponRepository _couponRepository;
private readonly IMapper<CouponDto, Coupon> _mapper;
public CouponController(ICouponRepository couponRepository, IMapper<CouponDto, Coupon> mapper)
{
_couponRepository = couponRepository;
_mapper = mapper;
}
// Add the GetCouponByCodeAsync method
}
}

+ 40
- 0
src/Services/Coupon/Coupon.API/Coupon.API.csproj View File

@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<UserSecretsId>1d5bc948-90f1-4906-a1f8-8edaa1ed9e2e</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..\..\..</DockerfileContext>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" Version="3.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="3.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" Version="3.0.5" />
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="3.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="3.0.2" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.13.1" />
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="3.1.2" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.9.10" />
<PackageReference Include="MongoDB.Driver" Version="2.10.2" />
<PackageReference Include="MongoDB.Driver.Core" Version="2.10.2" />
<PackageReference Include="Polly" Version="7.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.Http" Version="5.2.1" />
<PackageReference Include="Serilog.Sinks.Seq" Version="4.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" />
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusServiceBus\EventBusServiceBus.csproj" />
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" />
</ItemGroup>
</Project>

+ 15
- 0
src/Services/Coupon/Coupon.API/CouponSettings.cs View File

@ -0,0 +1,15 @@
namespace Coupon.API
{
public class CouponSettings
{
public string ConnectionString { get; set; }
public string CouponMongoDatabase { get; set; }
public string EventBusConnection { get; set; }
public bool UseCustomizationData { get; set; }
public bool AzureStorageEnabled { get; set; }
}
}

+ 25
- 0
src/Services/Coupon/Coupon.API/Dockerfile View File

@ -0,0 +1,25 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["src/Services/Coupon/Coupon.API/Coupon.API.csproj", "src/Services/Coupon/Coupon.API/"]
COPY ["src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj", "src/BuildingBlocks/EventBus/EventBusRabbitMQ/"]
COPY ["src/BuildingBlocks/EventBus/EventBus/EventBus.csproj", "src/BuildingBlocks/EventBus/EventBus/"]
COPY ["src/BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj", "src/BuildingBlocks/EventBus/EventBusServiceBus/"]
RUN dotnet restore "src/Services/Coupon/Coupon.API/Coupon.API.csproj"
COPY . .
WORKDIR "/src/src/Services/Coupon/Coupon.API"
RUN dotnet build "Coupon.API.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Coupon.API.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Coupon.API.dll"]

+ 9
- 0
src/Services/Coupon/Coupon.API/Dtos/CouponDto.cs View File

@ -0,0 +1,9 @@
namespace Coupon.API.Dtos
{
public class CouponDto
{
public int Discount { get; set; }
public string Code { get; set; }
}
}

+ 7
- 0
src/Services/Coupon/Coupon.API/Dtos/IMapper.cs View File

@ -0,0 +1,7 @@
namespace Coupon.API.Dtos
{
public interface IMapper<TResult, TEntity>
{
TResult Translate(TEntity entity);
}
}

+ 16
- 0
src/Services/Coupon/Coupon.API/Dtos/Mapper.cs View File

@ -0,0 +1,16 @@
namespace Coupon.API.Dtos
{
using Coupon.API.Infrastructure.Models;
public class Mapper : IMapper<CouponDto, Coupon>
{
public CouponDto Translate(Coupon entity)
{
return new CouponDto
{
Code = entity.Code,
Discount = entity.Discount
};
}
}
}

+ 49
- 0
src/Services/Coupon/Coupon.API/Extensions/IHostBuilderExtensions.cs View File

@ -0,0 +1,49 @@
using System;
using System.Data.SqlClient;
using Coupon.API.IntegrationEvents.EventHandlers;
using Coupon.API.IntegrationEvents.Events;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Polly;
namespace Coupon.API.Extensions
{
public static class IHostBuilderExtensions
{
public static IHost SeedDatabaseStrategy<TContext>(this IHost host, Action<TContext> seeder)
{
using (var scope = host.Services.CreateScope())
{
var context = scope.ServiceProvider.GetService<TContext>();
var policy = Policy.Handle<SqlException>()
.WaitAndRetry(new TimeSpan[]
{
TimeSpan.FromSeconds(3),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(8),
});
policy.Execute(() =>
{
seeder.Invoke(context);
});
}
return host;
}
public static IHost SubscribersIntegrationEvents(this IHost host)
{
using (var scope = host.Services.CreateScope())
{
var eventBus = scope.ServiceProvider.GetRequiredService<IEventBus>();
eventBus.Subscribe<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent, OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler>();
}
return host;
}
}
}

+ 227
- 0
src/Services/Coupon/Coupon.API/Extensions/IServiceCollectionExtensions.cs View File

@ -0,0 +1,227 @@
namespace Coupon.API.Extensions
{
using System;
using System.Collections.Generic;
using Autofac;
using Coupon.API.Dtos;
using Coupon.API.Filters;
using Coupon.API.Infrastructure.Models;
using Coupon.API.Infrastructure.Repositories;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.ServiceBus;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using RabbitMQ.Client;
public static class IServiceCollectionExtensions
{
public static IServiceCollection AddCouponRegister(this IServiceCollection services, IConfiguration configuration)
{
services.AddTransient<ICouponRepository, CouponRepository>()
.AddTransient<IServiceBusPersisterConnection, DefaultServiceBusPersisterConnection>(service =>
{
var connection = new ServiceBusConnectionStringBuilder(configuration["EventBusConnection"]);
return new DefaultServiceBusPersisterConnection(connection, service.GetService<ILogger<DefaultServiceBusPersisterConnection>>());
})
.AddTransient<IRabbitMQPersistentConnection, DefaultRabbitMQPersistentConnection>(service =>
{
var factory = new ConnectionFactory()
{
HostName = configuration["EventBusConnection"],
DispatchConsumersAsync = true
};
var retryCount = 5;
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"]))
{
retryCount = int.Parse(configuration["EventBusRetryCount"]);
}
return new DefaultRabbitMQPersistentConnection(factory, service.GetService<ILogger<DefaultRabbitMQPersistentConnection>>(), retryCount);
})
.AddTransient<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>()
.AddTransient<CouponContext>()
.AddTransient<IMapper<CouponDto, Coupon>, Mapper>();
return services;
}
public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration)
{
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "eShopOnContainers - Coupon HTTP API",
Version = "v1",
Description = "The Coupon Service HTTP API"
});
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows()
{
Implicit = new OpenApiOAuthFlow()
{
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"),
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"),
Scopes = new Dictionary<string, string>()
{
{ "coupon", "Coupon API" }
}
}
}
});
options.OperationFilter<AuthorizeCheckOperationFilter>();
});
return services;
}
public static IServiceCollection AddCustomSettings(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<CouponSettings>(configuration);
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Instance = context.HttpContext.Request.Path,
Status = StatusCodes.Status400BadRequest,
Detail = "Please refer to the errors property for additional details."
};
return new BadRequestObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json", "application/problem+xml" }
};
};
});
return services;
}
public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
{
var subscriptionClientName = configuration["SubscriptionClientName"];
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
{
services.AddSingleton<IEventBus, EventBusServiceBus>(sp =>
{
var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>();
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>();
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
return new EventBusServiceBus(serviceBusPersisterConnection, logger, eventBusSubcriptionsManager, subscriptionClientName, iLifetimeScope);
});
}
else
{
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp =>
{
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>();
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>();
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
var retryCount = 5;
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"]))
{
retryCount = int.Parse(configuration["EventBusRetryCount"]);
}
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount);
});
}
return services;
}
public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration)
{
var accountName = configuration.GetValue<string>("AzureStorageAccountName");
var accountKey = configuration.GetValue<string>("AzureStorageAccountKey");
var hcBuilder = services.AddHealthChecks();
hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy())
.AddMongoDb(
configuration["ConnectionString"],
name: "CouponCollection-check",
tags: new string[] { "couponcollection" });
if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
{
hcBuilder.AddAzureServiceBusTopic(
configuration["EventBusConnection"],
topicName: "eshop_event_bus",
name: "coupon-servicebus-check",
tags: new string[] { "servicebus" });
}
else
{
hcBuilder.AddRabbitMQ(
$"amqp://{configuration["EventBusConnection"]}",
name: "coupon-rabbitmqbus-check",
tags: new string[] { "rabbitmqbus" });
}
return services;
}
public static IServiceCollection AddCustomPolicies(this IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.SetIsOriginAllowed((host) => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
return services;
}
public static IServiceCollection AddAppInsights(this IServiceCollection services, IConfiguration configuration)
{
services.AddApplicationInsightsTelemetry(configuration);
services.AddApplicationInsightsKubernetesEnricher();
return services;
}
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
{
return services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = configuration["IdentityUrl"];
options.RequireHttpsMetadata = false;
options.Audience = "coupon";
}).Services;
}
public static IServiceCollection AddCustomAuthorization(this IServiceCollection services) => services.AddAuthorization();
}
}

+ 35
- 0
src/Services/Coupon/Coupon.API/Filters/AuthorizeCheckOperationFilter.cs View File

@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Coupon.API.Filters
{
public class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
if (!hasAuthorize) return;
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
var oAuthScheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
};
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[oAuthScheme] = new [] { "CouponApi" }
}
};
}
}
}

+ 16
- 0
src/Services/Coupon/Coupon.API/Filters/ValidateModelAttribute.cs View File

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Coupon.API.Filters
{
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
}

+ 52
- 0
src/Services/Coupon/Coupon.API/Infrastructure/CouponSeed.cs View File

@ -0,0 +1,52 @@
namespace Coupon.API.Infrastructure
{
using System.Collections.Generic;
using System.Threading.Tasks;
using Coupon.API.Infrastructure.Models;
using Coupon.API.Infrastructure.Repositories;
public class CouponSeed
{
public async Task SeedAsync(CouponContext context)
{
if (context.Coupons.EstimatedDocumentCount() == 0)
{
var coupons = new List<Coupon>
{
new Coupon
{
Code = "DISC-5",
Discount = 5
},
new Coupon
{
Code = "DISC-10",
Discount = 10
},
new Coupon
{
Code = "DISC-15",
Discount = 15
},
new Coupon
{
Code = "DISC-20",
Discount = 20
},
new Coupon
{
Code = "DISC-25",
Discount = 25
},
new Coupon
{
Code = "DISC-30",
Discount = 30
}
};
await context.Coupons.InsertManyAsync(coupons);
}
}
}
}

+ 20
- 0
src/Services/Coupon/Coupon.API/Infrastructure/Models/Coupon.cs View File

@ -0,0 +1,20 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Coupon.API.Infrastructure.Models
{
public class Coupon
{
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public int Discount { get; set; }
public string Code { get; set; }
public bool Consumed { get; set; }
public int OrderId { get; set; }
}
}

+ 25
- 0
src/Services/Coupon/Coupon.API/Infrastructure/Repositories/CouponContext.cs View File

@ -0,0 +1,25 @@
namespace Coupon.API.Infrastructure.Repositories
{
using Coupon.API.Infrastructure.Models;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
public class CouponContext
{
private readonly IMongoDatabase _database = null;
public CouponContext(IOptions<CouponSettings> settings)
{
var client = new MongoClient(settings.Value.ConnectionString);
if (client is null)
{
throw new MongoConfigurationException("Cannot connect to the database. The connection string is not valid or the database is not accessible");
}
_database = client.GetDatabase(settings.Value.CouponMongoDatabase);
}
public IMongoCollection<Coupon> Coupons => _database.GetCollection<Coupon>("CouponCollection");
}
}

+ 42
- 0
src/Services/Coupon/Coupon.API/Infrastructure/Repositories/CouponRepository.cs View File

@ -0,0 +1,42 @@
namespace Coupon.API.Infrastructure.Repositories
{
using System.Threading.Tasks;
using Coupon.API.Infrastructure.Models;
using MongoDB.Driver;
public class CouponRepository : ICouponRepository
{
private readonly CouponContext _couponContext;
public CouponRepository(CouponContext couponContext)
{
_couponContext = couponContext;
}
public async Task UpdateCouponConsumedByCodeAsync(string code, int orderId)
{
var filter = Builders<Coupon>.Filter.Eq("Code", code);
var update = Builders<Coupon>.Update
.Set(coupon => coupon.Consumed, true)
.Set(coupon => coupon.OrderId, orderId);
await _couponContext.Coupons.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = false });
}
public async Task UpdateCouponReleasedByOrderIdAsync(int orderId)
{
var filter = Builders<Coupon>.Filter.Eq("OrderId", orderId);
var update = Builders<Coupon>.Update
.Set(coupon => coupon.Consumed, false)
.Set(coupon => coupon.OrderId, 0);
await _couponContext.Coupons.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = false });
}
public async Task<Coupon> FindCouponByCodeAsync(string code)
{
var filter = Builders<Coupon>.Filter.Eq("Code", code);
return await _couponContext.Coupons.Find(filter).FirstOrDefaultAsync();
}
}
}

+ 14
- 0
src/Services/Coupon/Coupon.API/Infrastructure/Repositories/ICouponRepository.cs View File

@ -0,0 +1,14 @@
namespace Coupon.API.Infrastructure.Repositories
{
using System.Threading.Tasks;
using Coupon.API.Infrastructure.Models;
public interface ICouponRepository
{
Task<Coupon> FindCouponByCodeAsync(string code);
Task UpdateCouponConsumedByCodeAsync(string code, int orderId);
Task UpdateCouponReleasedByOrderIdAsync(int orderId);
}
}

+ 56
- 0
src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandlers/OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler.cs View File

@ -0,0 +1,56 @@
using System.Threading.Tasks;
using Coupon.API.Infrastructure.Repositories;
using Coupon.API.IntegrationEvents.Events;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using Serilog;
using Serilog.Context;
namespace Coupon.API.IntegrationEvents.EventHandlers
{
public class OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent>
{
private readonly ICouponRepository _couponRepository;
private readonly IEventBus _eventBus;
public OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler(ICouponRepository couponRepository, IEventBus eventBus)
{
_couponRepository = couponRepository;
_eventBus = eventBus;
}
public async Task Handle(OrderStatusChangedToAwaitingCouponValidationIntegrationEvent @event)
{
await Task.Delay(3000);
using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-Coupon.API"))
{
Log.Information("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, "Coupon.API", @event);
var couponIntegrationEvent = await ProcessIntegrationEventAsync(@event);
Log.Information("----- Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", couponIntegrationEvent.Id, "Coupon.API", couponIntegrationEvent);
_eventBus.Publish(couponIntegrationEvent);
}
}
private async Task<IntegrationEvent> ProcessIntegrationEventAsync(OrderStatusChangedToAwaitingCouponValidationIntegrationEvent integrationEvent)
{
var coupon = await _couponRepository.FindCouponByCodeAsync(integrationEvent.Code);
Log.Information("----- Coupon \"{CouponCode}\": {@Coupon}", integrationEvent.Code, coupon);
if (coupon == null || coupon.Consumed)
{
return new OrderCouponRejectedIntegrationEvent(integrationEvent.OrderId, coupon.Code);
}
Log.Information("Consumed coupon: {DiscountCode}", integrationEvent.Code);
await _couponRepository.UpdateCouponConsumedByCodeAsync(integrationEvent.Code, integrationEvent.OrderId);
return new OrderCouponConfirmedIntegrationEvent(integrationEvent.OrderId, coupon.Discount);
}
}
}

+ 22
- 0
src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandlers/OrderStatusChangedToCancelledIntegrationEventHandler.cs View File

@ -0,0 +1,22 @@
using System.Threading.Tasks;
using Coupon.API.Infrastructure.Repositories;
using Coupon.API.IntegrationEvents.Events;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
namespace Coupon.API.IntegrationEvents.EventHandlers
{
public class OrderStatusChangedToCancelledIntegrationEventHandler : IIntegrationEventHandler<OrderStatusChangedToCancelledIntegrationEvent>
{
private readonly ICouponRepository _couponRepository;
public OrderStatusChangedToCancelledIntegrationEventHandler(ICouponRepository couponRepository)
{
_couponRepository = couponRepository;
}
public async Task Handle(OrderStatusChangedToCancelledIntegrationEvent @event)
{
await _couponRepository.UpdateCouponReleasedByOrderIdAsync(@event.OrderId);
}
}
}

+ 17
- 0
src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs View File

@ -0,0 +1,17 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
namespace Coupon.API.IntegrationEvents.Events
{
public class OrderCouponConfirmedIntegrationEvent : IntegrationEvent
{
public int OrderId { get; }
public int Discount { get; }
public OrderCouponConfirmedIntegrationEvent(int orderId, int discount)
{
OrderId = orderId;
Discount = discount;
}
}
}

+ 17
- 0
src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs View File

@ -0,0 +1,17 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
namespace Coupon.API.IntegrationEvents.Events
{
public class OrderCouponRejectedIntegrationEvent : IntegrationEvent
{
public int OrderId { get; }
public string Code { get; }
public OrderCouponRejectedIntegrationEvent(int orderId, string code)
{
OrderId = orderId;
Code = code;
}
}
}

+ 20
- 0
src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs View File

@ -0,0 +1,20 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using Newtonsoft.Json;
namespace Coupon.API.IntegrationEvents.Events
{
public class OrderStatusChangedToAwaitingCouponValidationIntegrationEvent : IntegrationEvent
{
[JsonProperty]
public int OrderId { get; private set; }
[JsonProperty]
public string OrderStatus { get; private set; }
[JsonProperty]
public string BuyerName { get; private set; }
[JsonProperty]
public string Code { get; private set; }
}
}

+ 24
- 0
src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToCancelledIntegrationEvent.cs View File

@ -0,0 +1,24 @@
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Coupon.API.IntegrationEvents.Events
{
public class OrderStatusChangedToCancelledIntegrationEvent : IntegrationEvent
{
[JsonProperty]
public int OrderId { get; private set; }
[JsonProperty]
public string OrderStatus { get; private set; }
[JsonProperty]
public string BuyerName { get; private set; }
[JsonProperty]
public string DiscountCode { get; private set; }
}
}

+ 53
- 0
src/Services/Coupon/Coupon.API/Program.cs View File

@ -0,0 +1,53 @@
using System.IO;
using Autofac.Extensions.DependencyInjection;
using Coupon.API.Extensions;
using Coupon.API.Infrastructure;
using Coupon.API.Infrastructure.Repositories;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
namespace Coupon.API
{
public class Program
{
public static void Main(string[] args) =>
CreateHostBuilder(args)
.Build()
.SeedDatabaseStrategy<CouponContext>(context => new CouponSeed().SeedAsync(context).Wait())
.SubscribersIntegrationEvents()
.Run();
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureAppConfiguration((host, builder) =>
{
builder.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{host.HostingEnvironment.EnvironmentName}.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
var config = builder.Build();
if (config.GetValue("UseVault", false))
{
builder.AddAzureKeyVault($"https://{config["Vault:Name"]}.vault.azure.net/", config["Vault:ClientId"], config["Vault:ClientSecret"]);
}
})
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>())
.UseSerilog((host, builder) =>
{
builder.MinimumLevel.Verbose()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.WithProperty("ApplicationContext", host.HostingEnvironment.ApplicationName)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.Seq(string.IsNullOrWhiteSpace(host.Configuration["Serilog:SeqServerUrl"]) ? "http://seq" : host.Configuration["Serilog:SeqServerUrl"])
.WriteTo.Http(string.IsNullOrWhiteSpace(host.Configuration["Serilog:LogstashUrl"]) ? "http://logstash:8080" : host.Configuration["Serilog:LogstashUrl"])
.ReadFrom.Configuration(host.Configuration);
});
}
}

+ 27
- 0
src/Services/Coupon/Coupon.API/Properties/launchSettings.json View File

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50436/",
"sslPort": 44354
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Coupon.API": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

+ 85
- 0
src/Services/Coupon/Coupon.API/Startup.cs View File

@ -0,0 +1,85 @@
using Coupon.API.Extensions;
using Coupon.API.Filters;
using Coupon.API.IntegrationEvents.EventHandlers;
using Coupon.API.IntegrationEvents.Events;
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
namespace Coupon.API
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options => options.Filters.Add<ValidateModelAttribute>());
services.AddCustomSettings(Configuration)
.AddCouponRegister(Configuration)
.AddCustomPolicies()
.AddAppInsights(Configuration)
.AddEventBus(Configuration)
.AddCustomAuthentication(Configuration)
.AddCustomAuthorization()
.AddSwagger(Configuration);
services.AddTransient<IIntegrationEventHandler<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent>, OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler>();
services.AddTransient<IIntegrationEventHandler<OrderStatusChangedToCancelledIntegrationEvent>, OrderStatusChangedToCancelledIntegrationEventHandler>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
var pathBase = Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase))
{
app.UsePathBase(pathBase);
}
app.UseSwagger()
.UseSwaggerUI(options =>
{
options.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Coupon.API V1");
options.OAuthClientId("couponswaggerui");
options.OAuthAppName("eShop-Learn.Coupon.API Swagger UI");
})
.UseCors("CorsPolicy")
.UseRouting()
.UseAuthentication()
.UseAuthorization()
.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
// Add the endpoints.MapHealthChecks code
});
ConfigureEventBus(app);
}
private void ConfigureEventBus(IApplicationBuilder app)
{
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
eventBus.Subscribe<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent, IIntegrationEventHandler<OrderStatusChangedToAwaitingCouponValidationIntegrationEvent>>();
eventBus.Subscribe<OrderStatusChangedToCancelledIntegrationEvent, IIntegrationEventHandler<OrderStatusChangedToCancelledIntegrationEvent>>();
}
}
}

+ 18
- 0
src/Services/Coupon/Coupon.API/appsettings.Development.json View File

@ -0,0 +1,18 @@
{
"ConnectionString": "mongodb://localhost:27017",
"CouponMongoDatabase": "CouponDb",
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning",
"Microsoft.eShopOnContainers": "Debug",
"System": "Warning"
}
}
},
"IdentityUrlExternal": "http://localhost:5105",
"IdentityUrl": "http://localhost:5105",
"AzureServiceBusEnabled": false,
"EventBusConnection": "localhost"
}

+ 28
- 0
src/Services/Coupon/Coupon.API/appsettings.json View File

@ -0,0 +1,28 @@
{
"ConnectionString": null,
"CouponMongoDatabase": "CouponDb",
"UseCustomizationData": false,
"Serilog": {
"SeqServerUrl": null,
"LogstashUrl": null,
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.eShopOnContainers": "Information",
"System": "Warning"
}
}
},
"SubscriptionClientName": "Coupon",
"ApplicationInsights": {
"InstrumentationKey": ""
},
"EventBusRetryCount": 5,
"UseVault": false,
"Vault": {
"Name": "eshop",
"ClientId": "your-client-id",
"ClientSecret": "your-client-secret"
}
}

+ 56
- 2
src/eShopOnContainers-ServicesAndWebApps.sln View File

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29020.237
# Visual Studio Version 17
VisualStudioVersion = 17.0.32014.148
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{FEA0C318-FFED-4D39-8781-265718CA43DD}" Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{FEA0C318-FFED-4D39-8781-265718CA43DD}"
EndProject EndProject
@ -124,6 +124,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{373D8AA1
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventBus.Tests", "BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{95D735BE-2899-4495-BE3F-2600E93B4E3C}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventBus.Tests", "BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{95D735BE-2899-4495-BE3F-2600E93B4E3C}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Coupon", "Coupon", "{6E72EEF6-E86F-4880-B40E-60BA9349C422}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coupon.API", "Services\Coupon\Coupon.API\Coupon.API.csproj", "{76D98902-FB50-45F3-917F-023B23233174}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
@ -1530,6 +1534,54 @@ Global
{95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x64.Build.0 = Release|Any CPU {95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x64.Build.0 = Release|Any CPU
{95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.ActiveCfg = Release|Any CPU {95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.ActiveCfg = Release|Any CPU
{95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.Build.0 = Release|Any CPU {95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.Build.0 = Release|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Ad-Hoc|x64.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Ad-Hoc|x86.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.AppStore|ARM.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.AppStore|ARM.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.AppStore|iPhone.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.AppStore|x64.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.AppStore|x64.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.AppStore|x86.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.AppStore|x86.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Debug|ARM.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Debug|ARM.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Debug|iPhone.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Debug|x64.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Debug|x64.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Debug|x86.ActiveCfg = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Debug|x86.Build.0 = Debug|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Release|Any CPU.Build.0 = Release|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Release|ARM.ActiveCfg = Release|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Release|ARM.Build.0 = Release|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Release|iPhone.ActiveCfg = Release|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Release|iPhone.Build.0 = Release|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Release|x64.ActiveCfg = Release|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Release|x64.Build.0 = Release|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Release|x86.ActiveCfg = Release|Any CPU
{76D98902-FB50-45F3-917F-023B23233174}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -1588,6 +1640,8 @@ Global
{B62E859F-825E-4C8B-93EC-5966EACFD026} = {798BFC44-2CCD-45FA-B37A-5173B03C2B30} {B62E859F-825E-4C8B-93EC-5966EACFD026} = {798BFC44-2CCD-45FA-B37A-5173B03C2B30}
{373D8AA1-36BE-49EC-89F0-6CB736666285} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} {373D8AA1-36BE-49EC-89F0-6CB736666285} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F}
{95D735BE-2899-4495-BE3F-2600E93B4E3C} = {373D8AA1-36BE-49EC-89F0-6CB736666285} {95D735BE-2899-4495-BE3F-2600E93B4E3C} = {373D8AA1-36BE-49EC-89F0-6CB736666285}
{6E72EEF6-E86F-4880-B40E-60BA9349C422} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8}
{76D98902-FB50-45F3-917F-023B23233174} = {6E72EEF6-E86F-4880-B40E-60BA9349C422}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {25728519-5F0F-4973-8A64-0A81EB4EA8D9} SolutionGuid = {25728519-5F0F-4973-8A64-0A81EB4EA8D9}


Loading…
Cancel
Save