├── .gitignore ├── README.md ├── proto └── inventory.proto ├── purgeimages.sh ├── src └── PartialFoods.Services.InventoryServer │ ├── Dockerfile │ ├── Entities │ ├── IInventoryRepository.cs │ ├── InventoryContext.cs │ ├── InventoryRepository.cs │ ├── Product.cs │ └── ProductActivity.cs │ ├── InventoryManagementImpl.cs │ ├── InventoryReleasedEvent.cs │ ├── InventoryReleasedEventProcessor.cs │ ├── InventoryReservedEvent.cs │ ├── InventoryReservedEventProcessor.cs │ ├── KafkaReleasedConsumer.cs │ ├── KafkaReservedConsumer.cs │ ├── Migrations │ ├── 20171009152001_CreateDatabase.Designer.cs │ ├── 20171009152001_CreateDatabase.cs │ ├── 20171009163554_MoreKeys.Designer.cs │ ├── 20171009163554_MoreKeys.cs │ └── InventoryContextModelSnapshot.cs │ ├── PartialFoods.Services.InventoryServer.csproj │ ├── Program.cs │ ├── RPC │ ├── Inventory.cs │ └── InventoryGrpc.cs │ ├── appsettings.json │ └── makeprotos.sh └── tests └── PartialFoods.Services.InventoryServer.Tests ├── PartialFoods.Services.InventoryServer.Tests.csproj └── UnitTest1.cs /.gitignore: -------------------------------------------------------------------------------- 1 | project.lock.json 2 | *~ 3 | \#* 4 | bin 5 | obj 6 | .vscode/ 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | *.vscode/ 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | build/ 25 | publish/ 26 | bld/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | *.dll 30 | *.pdb 31 | 32 | # Visual Studo 2015 cache/options directory 33 | .vs/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inventory 2 | Inventory service for the "Partial Foods" sample demonstrating event sourcing and gRPC services in .NET Core. 3 | 4 | Once the server is running, you can query it with `grpcurl`: 5 | 6 | ``` 7 | $ grpcurl -k ls localhost:8082 PartialFoods.Services.InventoryManagement 8 | PartialFoods.Services.InventoryManagement.GetEffectiveQuantity 9 | PartialFoods.Services.InventoryManagement.GetActivity 10 | ``` 11 | 12 | Here's an example query of activity belonging to the SKU **ABC123**: 13 | 14 | ``` 15 | $ echo '{"SKU": "ABC123"}' | grpcurl -k call localhost:8082 PartialFoods.Services.InventoryManagement.GetActivity | jq 16 | { 17 | "Activities": [ 18 | { 19 | "SKU": "ABC123", 20 | "Timestamp": 1, 21 | "Quantity": 10, 22 | "ActivityType": "RESERVED", 23 | "ActivityID": "6b082670-1a4e-43f5-8a67-3c71a4c1feef", 24 | "OrderID": "DEMO" 25 | }, 26 | { 27 | "SKU": "ABC123", 28 | "Timestamp": 2, 29 | "Quantity": 10, 30 | "ActivityType": "RELEASED", 31 | "ActivityID": "9f68ab19-cef3-4e57-a1cd-c8b21cc060fd", 32 | "OrderID": "DEMO" 33 | } 34 | ] 35 | } 36 | ``` -------------------------------------------------------------------------------- /proto/inventory.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package PartialFoods.Services; 4 | 5 | service InventoryManagement { 6 | rpc GetEffectiveQuantity(GetProductRequest) returns (GetQuantityResponse); 7 | rpc GetActivity(GetProductRequest) returns (ActivityResponse); 8 | } 9 | 10 | message GetProductRequest { 11 | string SKU = 1; 12 | } 13 | 14 | message GetQuantityResponse { 15 | uint32 Quantity = 1; 16 | } 17 | 18 | message ActivityResponse { 19 | repeated Activity Activities = 1; 20 | } 21 | 22 | message Activity { 23 | string SKU = 1; 24 | uint64 Timestamp = 2; 25 | uint32 Quantity = 3; 26 | ActivityType ActivityType = 4; 27 | string ActivityID = 5; 28 | string OrderID = 6; 29 | } 30 | 31 | enum ActivityType { 32 | UNKNOWN = 0; 33 | RESERVED = 1; 34 | RELEASED = 2; 35 | SHIPPED = 3; 36 | STOCKADD = 4; 37 | } -------------------------------------------------------------------------------- /purgeimages.sh: -------------------------------------------------------------------------------- 1 | docker rmi $(docker images | grep "^" | awk '{print $3}') -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.0.5-sdk-2.1.4 as publish 2 | WORKDIR /publish 3 | COPY PartialFoods.Services.InventoryServer.csproj . 4 | RUN dotnet restore 5 | COPY . . 6 | RUN dotnet publish --output ./out 7 | 8 | FROM microsoft/dotnet:2.0.5-runtime 9 | WORKDIR /app 10 | COPY --from=publish /publish/out . 11 | ADD appsettings.json /app 12 | ENTRYPOINT ["dotnet", "PartialFoods.Services.InventoryServer.dll"] -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/Entities/IInventoryRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PartialFoods.Services.InventoryServer.Entities 4 | { 5 | public interface IInventoryRepository 6 | { 7 | ProductActivity PutActivity(ProductActivity activity); 8 | Product GetProduct(string sku); 9 | int GetCurrentQuantity(string sku); 10 | IList GetActivity(string sku); 11 | } 12 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/Entities/InventoryContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Configuration; 4 | 5 | namespace PartialFoods.Services.InventoryServer.Entities 6 | { 7 | public class InventoryContext : DbContext 8 | { 9 | public DbSet Products { get; set; } 10 | public DbSet Activities { get; set; } 11 | 12 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 13 | { 14 | optionsBuilder.UseNpgsql(Program.Configuration.GetConnectionString("inventory")); 15 | } 16 | 17 | protected override void OnModelCreating(ModelBuilder builder) 18 | { 19 | builder.Entity() 20 | .HasKey(p => p.SKU); 21 | 22 | builder.Entity() 23 | .HasKey(pa => new { pa.ActivityID }); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/Entities/InventoryRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace PartialFoods.Services.InventoryServer.Entities 6 | { 7 | public class InventoryRepository : IInventoryRepository 8 | { 9 | private InventoryContext context; 10 | 11 | public InventoryRepository(InventoryContext context) 12 | { 13 | this.context = context; 14 | } 15 | 16 | public int GetCurrentQuantity(string sku) 17 | { 18 | var quantity = 0; 19 | try 20 | { 21 | var productActivities = GetActivity(sku); 22 | foreach (var activity in productActivities) 23 | { 24 | if ((activity.ActivityType == ActivityType.Released) || 25 | (activity.ActivityType == ActivityType.StockAdd)) 26 | { 27 | quantity += activity.Quantity; 28 | } 29 | else if (activity.ActivityType == ActivityType.Reserved) 30 | { 31 | quantity -= activity.Quantity; 32 | } 33 | // Shipped activity doesn't change quantity 34 | } 35 | } 36 | catch (Exception ex) 37 | { 38 | Console.WriteLine(ex.StackTrace); 39 | Console.WriteLine($"Failed to query current quantity: {ex.ToString()}"); 40 | } 41 | return quantity; 42 | } 43 | 44 | public Product GetProduct(string sku) 45 | { 46 | try 47 | { 48 | var product = context.Products.FirstOrDefault(p => p.SKU == sku); 49 | return product; 50 | } 51 | catch (Exception ex) 52 | { 53 | Console.WriteLine(ex.StackTrace); 54 | Console.WriteLine($"Failed to query product - {sku}"); 55 | } 56 | return null; 57 | } 58 | 59 | public IList GetActivity(string sku) 60 | { 61 | var activities = (from activity in context.Activities 62 | where activity.SKU == sku 63 | orderby activity.CreatedOn ascending 64 | select activity).ToList(); 65 | return activities; 66 | } 67 | 68 | public ProductActivity PutActivity(ProductActivity activity) 69 | { 70 | Console.WriteLine($"Attempting to put activity {activity.ActivityID}, type {activity.ActivityType.ToString()}"); 71 | 72 | try 73 | { 74 | var existing = context.Activities.FirstOrDefault(a => a.ActivityID == activity.ActivityID); 75 | if (existing != null) 76 | { 77 | Console.WriteLine($"Bypassing add for order activity {activity.ActivityID} - already exists."); 78 | return activity; 79 | } 80 | context.Add(activity); 81 | context.SaveChanges(); 82 | } 83 | catch (Exception ex) 84 | { 85 | Console.WriteLine($"Failed to store activity {ex.ToString()}"); 86 | Console.WriteLine(ex.StackTrace); 87 | return null; 88 | } 89 | return activity; 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/Entities/Product.cs: -------------------------------------------------------------------------------- 1 | namespace PartialFoods.Services.InventoryServer.Entities 2 | { 3 | public class Product 4 | { 5 | public string SKU { get; set; } 6 | public int OriginalQuantity { get; set; } 7 | public string Name { get; set; } 8 | public string Description { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/Entities/ProductActivity.cs: -------------------------------------------------------------------------------- 1 | namespace PartialFoods.Services.InventoryServer.Entities 2 | { 3 | public class ProductActivity 4 | { 5 | public string SKU { get; set; } 6 | public string ActivityID { get; set; } 7 | 8 | public ActivityType ActivityType { get; set; } 9 | 10 | public long CreatedOn { get; set; } 11 | public string OrderID { get; set; } 12 | 13 | public int Quantity { get; set; } 14 | } 15 | 16 | public enum ActivityType 17 | { 18 | Reserved = 1, 19 | Released = 2, 20 | Shipped = 3, 21 | StockAdd = 4 22 | } 23 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/InventoryManagementImpl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using PartialFoods.Services; 5 | using PartialFoods.Services.InventoryServer.Entities; 6 | 7 | namespace PartialFoods.Services.InventoryServer 8 | { 9 | public class InventoryManagementImpl : InventoryManagement.InventoryManagementBase 10 | { 11 | private IInventoryRepository repository; 12 | 13 | private ILogger logger; 14 | 15 | public InventoryManagementImpl(IInventoryRepository repository, 16 | ILogger logger) 17 | { 18 | this.repository = repository; 19 | this.logger = logger; 20 | } 21 | 22 | public override Task GetEffectiveQuantity(GetProductRequest request, Grpc.Core.ServerCallContext context) 23 | { 24 | logger.LogInformation($"Received query for effective quantity of SKU {request.SKU}"); 25 | int quantity = repository.GetCurrentQuantity(request.SKU); 26 | return Task.FromResult(new GetQuantityResponse { Quantity = (uint)quantity }); 27 | } 28 | 29 | public override Task GetActivity(GetProductRequest request, Grpc.Core.ServerCallContext context) 30 | { 31 | logger.LogInformation($"Received query for product activity for SKU {request.SKU}"); 32 | var response = new ActivityResponse(); 33 | try 34 | { 35 | var activities = this.repository.GetActivity(request.SKU); 36 | foreach (var activity in activities) 37 | { 38 | response.Activities.Add(new PartialFoods.Services.Activity 39 | { 40 | SKU = activity.SKU, 41 | ActivityID = activity.ActivityID, 42 | Timestamp = (ulong)activity.CreatedOn, 43 | OrderID = activity.OrderID, 44 | Quantity = (uint)activity.Quantity, 45 | ActivityType = ToProtoActivityType(activity.ActivityType) 46 | }); 47 | } 48 | return Task.FromResult(response); 49 | } 50 | catch (Exception ex) 51 | { 52 | logger.LogError(ex, "Failed to retrieve activity for SKU : {}", request.SKU); 53 | return (Task)Task.FromException(ex); 54 | } 55 | } 56 | 57 | private PartialFoods.Services.ActivityType ToProtoActivityType( 58 | PartialFoods.Services.InventoryServer.Entities.ActivityType at) 59 | { 60 | if (at == PartialFoods.Services.InventoryServer.Entities.ActivityType.Released) 61 | { 62 | return PartialFoods.Services.ActivityType.Released; 63 | } 64 | else if (at == PartialFoods.Services.InventoryServer.Entities.ActivityType.Shipped) 65 | { 66 | return PartialFoods.Services.ActivityType.Shipped; 67 | } 68 | else if (at == PartialFoods.Services.InventoryServer.Entities.ActivityType.Reserved) 69 | { 70 | return PartialFoods.Services.ActivityType.Reserved; 71 | } 72 | else if (at == PartialFoods.Services.InventoryServer.Entities.ActivityType.StockAdd) 73 | { 74 | return PartialFoods.Services.ActivityType.Stockadd; 75 | } 76 | else 77 | { 78 | return PartialFoods.Services.ActivityType.Unknown; 79 | } 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/InventoryReleasedEvent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PartialFoods.Services.InventoryServer 4 | { 5 | public class InventoryReleasedEvent 6 | { 7 | [JsonProperty("sku")] 8 | public string SKU { get; set; } 9 | 10 | [JsonProperty("quantity")] 11 | public uint Quantity { get; set; } 12 | 13 | [JsonProperty("released_on")] 14 | public ulong ReservedOn { get; set; } 15 | 16 | [JsonProperty("order_id")] 17 | public string OrderID { get; set; } 18 | 19 | [JsonProperty("user_id")] 20 | public string UserID { get; set; } 21 | 22 | [JsonProperty("event_id")] 23 | public string EventID { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/InventoryReleasedEventProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PartialFoods.Services.InventoryServer.Entities; 3 | 4 | namespace PartialFoods.Services.InventoryServer 5 | { 6 | public class InventoryReleasedEventProcessor 7 | { 8 | private IInventoryRepository repository; 9 | 10 | public InventoryReleasedEventProcessor(IInventoryRepository repository) 11 | { 12 | this.repository = repository; 13 | } 14 | 15 | public bool HandleInventoryReleasedEvent(InventoryReleasedEvent evt) 16 | { 17 | Console.WriteLine($"Handling inventory released event - {evt.EventID}"); 18 | ProductActivity activity = new ProductActivity 19 | { 20 | OrderID = evt.OrderID, 21 | SKU = evt.SKU, 22 | Quantity = (int)evt.Quantity, 23 | ActivityID = evt.EventID, 24 | CreatedOn = DateTime.UtcNow.Ticks, 25 | ActivityType = PartialFoods.Services.InventoryServer.Entities.ActivityType.Released 26 | }; 27 | var result = repository.PutActivity(activity); 28 | 29 | return (result != null); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/InventoryReservedEvent.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PartialFoods.Services.InventoryServer 4 | { 5 | public class InventoryReservedEvent 6 | { 7 | [JsonProperty("sku")] 8 | public string SKU { get; set; } 9 | 10 | [JsonProperty("quantity")] 11 | public uint Quantity { get; set; } 12 | 13 | [JsonProperty("reserved_on")] 14 | public ulong ReservedOn { get; set; } 15 | 16 | [JsonProperty("order_id")] 17 | public string OrderID { get; set; } 18 | 19 | [JsonProperty("user_id")] 20 | public string UserID { get; set; } 21 | 22 | [JsonProperty("event_id")] 23 | public string EventID { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/InventoryReservedEventProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PartialFoods.Services.InventoryServer.Entities; 3 | 4 | namespace PartialFoods.Services.InventoryServer 5 | { 6 | public class InventoryReservedEventProcessor 7 | { 8 | private IInventoryRepository repository; 9 | 10 | public InventoryReservedEventProcessor(IInventoryRepository repository) 11 | { 12 | this.repository = repository; 13 | } 14 | 15 | public bool HandleInventoryReservedEvent(InventoryReservedEvent evt) 16 | { 17 | Console.WriteLine($"Handling inventory reserved event - {evt.EventID}"); 18 | ProductActivity activity = new ProductActivity 19 | { 20 | OrderID = evt.OrderID, 21 | SKU = evt.SKU, 22 | Quantity = (int)evt.Quantity, 23 | ActivityID = evt.EventID, 24 | CreatedOn = DateTime.UtcNow.Ticks, 25 | ActivityType = PartialFoods.Services.InventoryServer.Entities.ActivityType.Reserved 26 | }; 27 | var result = repository.PutActivity(activity); 28 | 29 | return (result != null); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/KafkaReleasedConsumer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Confluent.Kafka; 6 | using Confluent.Kafka.Serialization; 7 | using Newtonsoft.Json; 8 | 9 | namespace PartialFoods.Services.InventoryServer 10 | { 11 | 12 | public class KafkaReleasedConsumer 13 | { 14 | private string topic; 15 | private Dictionary config; 16 | private InventoryReleasedEventProcessor eventProcessor; 17 | 18 | public KafkaReleasedConsumer(string topic, Dictionary config, InventoryReleasedEventProcessor eventProcessor) 19 | { 20 | this.topic = topic; 21 | this.config = config; 22 | this.eventProcessor = eventProcessor; 23 | } 24 | 25 | public void Consume() 26 | { 27 | Task.Run(() => 28 | { 29 | Console.WriteLine($"Starting Kafka subscription to {topic}"); 30 | using (var consumer = new Consumer(config, null, new StringDeserializer(Encoding.UTF8))) 31 | { 32 | //consumer.Assign(new List { new TopicPartitionOffset(topic, 0, 0) }); 33 | consumer.Subscribe(new[] { topic }); 34 | 35 | while (true) 36 | { 37 | Message msg; 38 | if (consumer.Consume(out msg, TimeSpan.FromSeconds(1))) 39 | { 40 | string rawJson = msg.Value; 41 | try 42 | { 43 | InventoryReleasedEvent evt = JsonConvert.DeserializeObject(rawJson); 44 | eventProcessor.HandleInventoryReleasedEvent(evt); 45 | var committedOffsets = consumer.CommitAsync(msg).Result; 46 | if (committedOffsets.Error.HasError) 47 | { 48 | Console.WriteLine($"Failed to commit offsets : {committedOffsets.Error.Reason}"); 49 | } 50 | } 51 | catch (Exception ex) 52 | { 53 | Console.WriteLine(ex.StackTrace); 54 | Console.WriteLine($"Failed to handle inventory released event : ${ex.ToString()}"); 55 | } 56 | } 57 | } 58 | } 59 | }); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/KafkaReservedConsumer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Confluent.Kafka; 6 | using Confluent.Kafka.Serialization; 7 | using Newtonsoft.Json; 8 | 9 | namespace PartialFoods.Services.InventoryServer 10 | { 11 | 12 | public class KafkaReservedConsumer 13 | { 14 | private string topic; 15 | private Dictionary config; 16 | private InventoryReservedEventProcessor eventProcessor; 17 | 18 | public KafkaReservedConsumer(string topic, Dictionary config, InventoryReservedEventProcessor eventProcessor) 19 | { 20 | this.topic = topic; 21 | this.config = config; 22 | this.eventProcessor = eventProcessor; 23 | } 24 | 25 | public void Consume() 26 | { 27 | Task.Run(() => 28 | { 29 | Console.WriteLine($"Starting Kafka subscription to {topic}"); 30 | using (var consumer = new Consumer(config, null, new StringDeserializer(Encoding.UTF8))) 31 | { 32 | //consumer.Assign(new List { new TopicPartitionOffset(topic, 0, 0) }); 33 | consumer.Subscribe(new[] { topic }); 34 | 35 | while (true) 36 | { 37 | Message msg; 38 | if (consumer.Consume(out msg, TimeSpan.FromSeconds(1))) 39 | { 40 | string rawJson = msg.Value; 41 | try 42 | { 43 | InventoryReservedEvent evt = JsonConvert.DeserializeObject(rawJson); 44 | eventProcessor.HandleInventoryReservedEvent(evt); 45 | var committedOffsets = consumer.CommitAsync(msg).Result; 46 | if (committedOffsets.Error.HasError) 47 | { 48 | Console.WriteLine($"Failed to commit offsets : {committedOffsets.Error.Reason}"); 49 | } 50 | } 51 | catch (Exception ex) 52 | { 53 | Console.WriteLine(ex.StackTrace); 54 | Console.WriteLine($"Failed to handle inventory reserved event : ${ex.ToString()}"); 55 | } 56 | } 57 | } 58 | } 59 | }); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/Migrations/20171009152001_CreateDatabase.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage; 7 | using Microsoft.EntityFrameworkCore.Storage.Internal; 8 | using PartialFoods.Services.InventoryServer.Entities; 9 | using System; 10 | 11 | namespace PartialFoods.Services.InventoryServer.Migrations 12 | { 13 | [DbContext(typeof(InventoryContext))] 14 | [Migration("20171009152001_CreateDatabase")] 15 | partial class CreateDatabase 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder 21 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) 22 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); 23 | 24 | modelBuilder.Entity("PartialFoods.Services.InventoryServer.Entities.Product", b => 25 | { 26 | b.Property("SKU") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("Description"); 30 | 31 | b.Property("Name"); 32 | 33 | b.Property("OriginalQuantity"); 34 | 35 | b.HasKey("SKU"); 36 | 37 | b.ToTable("Products"); 38 | }); 39 | 40 | modelBuilder.Entity("PartialFoods.Services.InventoryServer.Entities.ProductActivity", b => 41 | { 42 | b.Property("ActivityID") 43 | .ValueGeneratedOnAdd(); 44 | 45 | b.Property("ActivityType"); 46 | 47 | b.Property("CreatedOn"); 48 | 49 | b.Property("SKU"); 50 | 51 | b.HasKey("ActivityID"); 52 | 53 | b.ToTable("Activities"); 54 | }); 55 | #pragma warning restore 612, 618 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/Migrations/20171009152001_CreateDatabase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace PartialFoods.Services.InventoryServer.Migrations 6 | { 7 | public partial class CreateDatabase : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Activities", 13 | columns: table => new 14 | { 15 | ActivityID = table.Column(type: "text", nullable: false), 16 | ActivityType = table.Column(type: "int4", nullable: false), 17 | CreatedOn = table.Column(type: "int8", nullable: false), 18 | SKU = table.Column(type: "text", nullable: true) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Activities", x => x.ActivityID); 23 | }); 24 | 25 | migrationBuilder.CreateTable( 26 | name: "Products", 27 | columns: table => new 28 | { 29 | SKU = table.Column(type: "text", nullable: false), 30 | Description = table.Column(type: "text", nullable: true), 31 | Name = table.Column(type: "text", nullable: true), 32 | OriginalQuantity = table.Column(type: "int4", nullable: false) 33 | }, 34 | constraints: table => 35 | { 36 | table.PrimaryKey("PK_Products", x => x.SKU); 37 | }); 38 | } 39 | 40 | protected override void Down(MigrationBuilder migrationBuilder) 41 | { 42 | migrationBuilder.DropTable( 43 | name: "Activities"); 44 | 45 | migrationBuilder.DropTable( 46 | name: "Products"); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/Migrations/20171009163554_MoreKeys.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage; 7 | using Microsoft.EntityFrameworkCore.Storage.Internal; 8 | using PartialFoods.Services.InventoryServer.Entities; 9 | using System; 10 | 11 | namespace PartialFoods.Services.InventoryServer.Migrations 12 | { 13 | [DbContext(typeof(InventoryContext))] 14 | [Migration("20171009163554_MoreKeys")] 15 | partial class MoreKeys 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder 21 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) 22 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); 23 | 24 | modelBuilder.Entity("PartialFoods.Services.InventoryServer.Entities.Product", b => 25 | { 26 | b.Property("SKU") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("Description"); 30 | 31 | b.Property("Name"); 32 | 33 | b.Property("OriginalQuantity"); 34 | 35 | b.HasKey("SKU"); 36 | 37 | b.ToTable("Products"); 38 | }); 39 | 40 | modelBuilder.Entity("PartialFoods.Services.InventoryServer.Entities.ProductActivity", b => 41 | { 42 | b.Property("ActivityID") 43 | .ValueGeneratedOnAdd(); 44 | 45 | b.Property("ActivityType"); 46 | 47 | b.Property("CreatedOn"); 48 | 49 | b.Property("OrderID"); 50 | 51 | b.Property("Quantity"); 52 | 53 | b.Property("SKU"); 54 | 55 | b.HasKey("ActivityID"); 56 | 57 | b.ToTable("Activities"); 58 | }); 59 | #pragma warning restore 612, 618 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/Migrations/20171009163554_MoreKeys.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace PartialFoods.Services.InventoryServer.Migrations 6 | { 7 | public partial class MoreKeys : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.AddColumn( 12 | name: "OrderID", 13 | table: "Activities", 14 | type: "text", 15 | nullable: true); 16 | 17 | migrationBuilder.AddColumn( 18 | name: "Quantity", 19 | table: "Activities", 20 | type: "int4", 21 | nullable: false, 22 | defaultValue: 0); 23 | } 24 | 25 | protected override void Down(MigrationBuilder migrationBuilder) 26 | { 27 | migrationBuilder.DropColumn( 28 | name: "OrderID", 29 | table: "Activities"); 30 | 31 | migrationBuilder.DropColumn( 32 | name: "Quantity", 33 | table: "Activities"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/Migrations/InventoryContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage; 7 | using Microsoft.EntityFrameworkCore.Storage.Internal; 8 | using PartialFoods.Services.InventoryServer.Entities; 9 | using System; 10 | 11 | namespace PartialFoods.Services.InventoryServer.Migrations 12 | { 13 | [DbContext(typeof(InventoryContext))] 14 | partial class InventoryContextModelSnapshot : ModelSnapshot 15 | { 16 | protected override void BuildModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) 21 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); 22 | 23 | modelBuilder.Entity("PartialFoods.Services.InventoryServer.Entities.Product", b => 24 | { 25 | b.Property("SKU") 26 | .ValueGeneratedOnAdd(); 27 | 28 | b.Property("Description"); 29 | 30 | b.Property("Name"); 31 | 32 | b.Property("OriginalQuantity"); 33 | 34 | b.HasKey("SKU"); 35 | 36 | b.ToTable("Products"); 37 | }); 38 | 39 | modelBuilder.Entity("PartialFoods.Services.InventoryServer.Entities.ProductActivity", b => 40 | { 41 | b.Property("ActivityID") 42 | .ValueGeneratedOnAdd(); 43 | 44 | b.Property("ActivityType"); 45 | 46 | b.Property("CreatedOn"); 47 | 48 | b.Property("OrderID"); 49 | 50 | b.Property("Quantity"); 51 | 52 | b.Property("SKU"); 53 | 54 | b.HasKey("ActivityID"); 55 | 56 | b.ToTable("Activities"); 57 | }); 58 | #pragma warning restore 612, 618 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/PartialFoods.Services.InventoryServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using Grpc.Core; 5 | using System.Collections.Generic; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Logging; 8 | using PartialFoods.Services; 9 | using PartialFoods.Services.InventoryServer.Entities; 10 | using Grpc.Reflection.V1Alpha; 11 | using Grpc.Reflection; 12 | 13 | namespace PartialFoods.Services.InventoryServer 14 | { 15 | class Program 16 | { 17 | private static ManualResetEvent mre = new ManualResetEvent(false); 18 | 19 | public static IConfigurationRoot Configuration { get; set; } 20 | 21 | static void Main(string[] args) 22 | { 23 | var builder = new ConfigurationBuilder() 24 | .SetBasePath(Directory.GetCurrentDirectory()) 25 | .AddJsonFile("appsettings.json") 26 | .AddEnvironmentVariables(); 27 | 28 | Configuration = builder.Build(); 29 | 30 | ILoggerFactory loggerFactory = new LoggerFactory() 31 | .AddConsole() 32 | .AddDebug(); 33 | 34 | ILogger logger = loggerFactory.CreateLogger(); 35 | var rpcLogger = loggerFactory.CreateLogger(); 36 | 37 | var port = int.Parse(Configuration["service:port"]); 38 | 39 | string brokerList = Configuration["kafkaclient:brokerlist"]; 40 | const string reservedTopic = "inventoryreserved"; 41 | const string releasedTopic = "inventoryreleased"; 42 | 43 | var config = new Dictionary 44 | { 45 | { "group.id", "inventory-server" }, 46 | { "enable.auto.commit", false }, 47 | { "bootstrap.servers", brokerList } 48 | }; 49 | //var context = new InventoryContext(Configuration["postgres:connectionstring"]); 50 | var context = new InventoryContext(); 51 | var repo = new InventoryRepository(context); 52 | 53 | var reservedEventProcessor = new InventoryReservedEventProcessor(repo); 54 | var kafkaConsumer = new KafkaReservedConsumer(reservedTopic, config, reservedEventProcessor); 55 | kafkaConsumer.Consume(); 56 | 57 | var releasedEventProcessor = new InventoryReleasedEventProcessor(repo); 58 | var releasedConsumer = new KafkaReleasedConsumer(releasedTopic, config, releasedEventProcessor); 59 | releasedConsumer.Consume(); 60 | 61 | var refImpl = new ReflectionServiceImpl( 62 | ServerReflection.Descriptor, InventoryManagement.Descriptor); 63 | var inventoryManagement = new InventoryManagementImpl(repo, rpcLogger); 64 | Server server = new Server 65 | { 66 | Services = { InventoryManagement.BindService(inventoryManagement), 67 | ServerReflection.BindService(refImpl)}, 68 | Ports = { new ServerPort("localhost", port, ServerCredentials.Insecure) } 69 | }; 70 | server.Start(); 71 | logger.LogInformation("Inventory gRPC Service Listening on Port " + port); 72 | 73 | mre.WaitOne(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/RPC/Inventory.cs: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: inventory.proto 3 | #pragma warning disable 1591, 0612, 3021 4 | #region Designer generated code 5 | 6 | using pb = global::Google.Protobuf; 7 | using pbc = global::Google.Protobuf.Collections; 8 | using pbr = global::Google.Protobuf.Reflection; 9 | using scg = global::System.Collections.Generic; 10 | namespace PartialFoods.Services { 11 | 12 | /// Holder for reflection information generated from inventory.proto 13 | public static partial class InventoryReflection { 14 | 15 | #region Descriptor 16 | /// File descriptor for inventory.proto 17 | public static pbr::FileDescriptor Descriptor { 18 | get { return descriptor; } 19 | } 20 | private static pbr::FileDescriptor descriptor; 21 | 22 | static InventoryReflection() { 23 | byte[] descriptorData = global::System.Convert.FromBase64String( 24 | string.Concat( 25 | "Cg9pbnZlbnRvcnkucHJvdG8SFVBhcnRpYWxGb29kcy5TZXJ2aWNlcyIgChFH", 26 | "ZXRQcm9kdWN0UmVxdWVzdBILCgNTS1UYASABKAkiJwoTR2V0UXVhbnRpdHlS", 27 | "ZXNwb25zZRIQCghRdWFudGl0eRgBIAEoDSJHChBBY3Rpdml0eVJlc3BvbnNl", 28 | "EjMKCkFjdGl2aXRpZXMYASADKAsyHy5QYXJ0aWFsRm9vZHMuU2VydmljZXMu", 29 | "QWN0aXZpdHkinAEKCEFjdGl2aXR5EgsKA1NLVRgBIAEoCRIRCglUaW1lc3Rh", 30 | "bXAYAiABKAQSEAoIUXVhbnRpdHkYAyABKA0SOQoMQWN0aXZpdHlUeXBlGAQg", 31 | "ASgOMiMuUGFydGlhbEZvb2RzLlNlcnZpY2VzLkFjdGl2aXR5VHlwZRISCgpB", 32 | "Y3Rpdml0eUlEGAUgASgJEg8KB09yZGVySUQYBiABKAkqUgoMQWN0aXZpdHlU", 33 | "eXBlEgsKB1VOS05PV04QABIMCghSRVNFUlZFRBABEgwKCFJFTEVBU0VEEAIS", 34 | "CwoHU0hJUFBFRBADEgwKCFNUT0NLQUREEAQy5QEKE0ludmVudG9yeU1hbmFn", 35 | "ZW1lbnQSbAoUR2V0RWZmZWN0aXZlUXVhbnRpdHkSKC5QYXJ0aWFsRm9vZHMu", 36 | "U2VydmljZXMuR2V0UHJvZHVjdFJlcXVlc3QaKi5QYXJ0aWFsRm9vZHMuU2Vy", 37 | "dmljZXMuR2V0UXVhbnRpdHlSZXNwb25zZRJgCgtHZXRBY3Rpdml0eRIoLlBh", 38 | "cnRpYWxGb29kcy5TZXJ2aWNlcy5HZXRQcm9kdWN0UmVxdWVzdBonLlBhcnRp", 39 | "YWxGb29kcy5TZXJ2aWNlcy5BY3Rpdml0eVJlc3BvbnNlYgZwcm90bzM=")); 40 | descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, 41 | new pbr::FileDescriptor[] { }, 42 | new pbr::GeneratedClrTypeInfo(new[] {typeof(global::PartialFoods.Services.ActivityType), }, new pbr::GeneratedClrTypeInfo[] { 43 | new pbr::GeneratedClrTypeInfo(typeof(global::PartialFoods.Services.GetProductRequest), global::PartialFoods.Services.GetProductRequest.Parser, new[]{ "SKU" }, null, null, null), 44 | new pbr::GeneratedClrTypeInfo(typeof(global::PartialFoods.Services.GetQuantityResponse), global::PartialFoods.Services.GetQuantityResponse.Parser, new[]{ "Quantity" }, null, null, null), 45 | new pbr::GeneratedClrTypeInfo(typeof(global::PartialFoods.Services.ActivityResponse), global::PartialFoods.Services.ActivityResponse.Parser, new[]{ "Activities" }, null, null, null), 46 | new pbr::GeneratedClrTypeInfo(typeof(global::PartialFoods.Services.Activity), global::PartialFoods.Services.Activity.Parser, new[]{ "SKU", "Timestamp", "Quantity", "ActivityType", "ActivityID", "OrderID" }, null, null, null) 47 | })); 48 | } 49 | #endregion 50 | 51 | } 52 | #region Enums 53 | public enum ActivityType { 54 | [pbr::OriginalName("UNKNOWN")] Unknown = 0, 55 | [pbr::OriginalName("RESERVED")] Reserved = 1, 56 | [pbr::OriginalName("RELEASED")] Released = 2, 57 | [pbr::OriginalName("SHIPPED")] Shipped = 3, 58 | [pbr::OriginalName("STOCKADD")] Stockadd = 4, 59 | } 60 | 61 | #endregion 62 | 63 | #region Messages 64 | public sealed partial class GetProductRequest : pb::IMessage { 65 | private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new GetProductRequest()); 66 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 67 | public static pb::MessageParser Parser { get { return _parser; } } 68 | 69 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 70 | public static pbr::MessageDescriptor Descriptor { 71 | get { return global::PartialFoods.Services.InventoryReflection.Descriptor.MessageTypes[0]; } 72 | } 73 | 74 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 75 | pbr::MessageDescriptor pb::IMessage.Descriptor { 76 | get { return Descriptor; } 77 | } 78 | 79 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 80 | public GetProductRequest() { 81 | OnConstruction(); 82 | } 83 | 84 | partial void OnConstruction(); 85 | 86 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 87 | public GetProductRequest(GetProductRequest other) : this() { 88 | sKU_ = other.sKU_; 89 | } 90 | 91 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 92 | public GetProductRequest Clone() { 93 | return new GetProductRequest(this); 94 | } 95 | 96 | /// Field number for the "SKU" field. 97 | public const int SKUFieldNumber = 1; 98 | private string sKU_ = ""; 99 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 100 | public string SKU { 101 | get { return sKU_; } 102 | set { 103 | sKU_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); 104 | } 105 | } 106 | 107 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 108 | public override bool Equals(object other) { 109 | return Equals(other as GetProductRequest); 110 | } 111 | 112 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 113 | public bool Equals(GetProductRequest other) { 114 | if (ReferenceEquals(other, null)) { 115 | return false; 116 | } 117 | if (ReferenceEquals(other, this)) { 118 | return true; 119 | } 120 | if (SKU != other.SKU) return false; 121 | return true; 122 | } 123 | 124 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 125 | public override int GetHashCode() { 126 | int hash = 1; 127 | if (SKU.Length != 0) hash ^= SKU.GetHashCode(); 128 | return hash; 129 | } 130 | 131 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 132 | public override string ToString() { 133 | return pb::JsonFormatter.ToDiagnosticString(this); 134 | } 135 | 136 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 137 | public void WriteTo(pb::CodedOutputStream output) { 138 | if (SKU.Length != 0) { 139 | output.WriteRawTag(10); 140 | output.WriteString(SKU); 141 | } 142 | } 143 | 144 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 145 | public int CalculateSize() { 146 | int size = 0; 147 | if (SKU.Length != 0) { 148 | size += 1 + pb::CodedOutputStream.ComputeStringSize(SKU); 149 | } 150 | return size; 151 | } 152 | 153 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 154 | public void MergeFrom(GetProductRequest other) { 155 | if (other == null) { 156 | return; 157 | } 158 | if (other.SKU.Length != 0) { 159 | SKU = other.SKU; 160 | } 161 | } 162 | 163 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 164 | public void MergeFrom(pb::CodedInputStream input) { 165 | uint tag; 166 | while ((tag = input.ReadTag()) != 0) { 167 | switch(tag) { 168 | default: 169 | input.SkipLastField(); 170 | break; 171 | case 10: { 172 | SKU = input.ReadString(); 173 | break; 174 | } 175 | } 176 | } 177 | } 178 | 179 | } 180 | 181 | public sealed partial class GetQuantityResponse : pb::IMessage { 182 | private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new GetQuantityResponse()); 183 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 184 | public static pb::MessageParser Parser { get { return _parser; } } 185 | 186 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 187 | public static pbr::MessageDescriptor Descriptor { 188 | get { return global::PartialFoods.Services.InventoryReflection.Descriptor.MessageTypes[1]; } 189 | } 190 | 191 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 192 | pbr::MessageDescriptor pb::IMessage.Descriptor { 193 | get { return Descriptor; } 194 | } 195 | 196 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 197 | public GetQuantityResponse() { 198 | OnConstruction(); 199 | } 200 | 201 | partial void OnConstruction(); 202 | 203 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 204 | public GetQuantityResponse(GetQuantityResponse other) : this() { 205 | quantity_ = other.quantity_; 206 | } 207 | 208 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 209 | public GetQuantityResponse Clone() { 210 | return new GetQuantityResponse(this); 211 | } 212 | 213 | /// Field number for the "Quantity" field. 214 | public const int QuantityFieldNumber = 1; 215 | private uint quantity_; 216 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 217 | public uint Quantity { 218 | get { return quantity_; } 219 | set { 220 | quantity_ = value; 221 | } 222 | } 223 | 224 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 225 | public override bool Equals(object other) { 226 | return Equals(other as GetQuantityResponse); 227 | } 228 | 229 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 230 | public bool Equals(GetQuantityResponse other) { 231 | if (ReferenceEquals(other, null)) { 232 | return false; 233 | } 234 | if (ReferenceEquals(other, this)) { 235 | return true; 236 | } 237 | if (Quantity != other.Quantity) return false; 238 | return true; 239 | } 240 | 241 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 242 | public override int GetHashCode() { 243 | int hash = 1; 244 | if (Quantity != 0) hash ^= Quantity.GetHashCode(); 245 | return hash; 246 | } 247 | 248 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 249 | public override string ToString() { 250 | return pb::JsonFormatter.ToDiagnosticString(this); 251 | } 252 | 253 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 254 | public void WriteTo(pb::CodedOutputStream output) { 255 | if (Quantity != 0) { 256 | output.WriteRawTag(8); 257 | output.WriteUInt32(Quantity); 258 | } 259 | } 260 | 261 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 262 | public int CalculateSize() { 263 | int size = 0; 264 | if (Quantity != 0) { 265 | size += 1 + pb::CodedOutputStream.ComputeUInt32Size(Quantity); 266 | } 267 | return size; 268 | } 269 | 270 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 271 | public void MergeFrom(GetQuantityResponse other) { 272 | if (other == null) { 273 | return; 274 | } 275 | if (other.Quantity != 0) { 276 | Quantity = other.Quantity; 277 | } 278 | } 279 | 280 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 281 | public void MergeFrom(pb::CodedInputStream input) { 282 | uint tag; 283 | while ((tag = input.ReadTag()) != 0) { 284 | switch(tag) { 285 | default: 286 | input.SkipLastField(); 287 | break; 288 | case 8: { 289 | Quantity = input.ReadUInt32(); 290 | break; 291 | } 292 | } 293 | } 294 | } 295 | 296 | } 297 | 298 | public sealed partial class ActivityResponse : pb::IMessage { 299 | private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new ActivityResponse()); 300 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 301 | public static pb::MessageParser Parser { get { return _parser; } } 302 | 303 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 304 | public static pbr::MessageDescriptor Descriptor { 305 | get { return global::PartialFoods.Services.InventoryReflection.Descriptor.MessageTypes[2]; } 306 | } 307 | 308 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 309 | pbr::MessageDescriptor pb::IMessage.Descriptor { 310 | get { return Descriptor; } 311 | } 312 | 313 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 314 | public ActivityResponse() { 315 | OnConstruction(); 316 | } 317 | 318 | partial void OnConstruction(); 319 | 320 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 321 | public ActivityResponse(ActivityResponse other) : this() { 322 | activities_ = other.activities_.Clone(); 323 | } 324 | 325 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 326 | public ActivityResponse Clone() { 327 | return new ActivityResponse(this); 328 | } 329 | 330 | /// Field number for the "Activities" field. 331 | public const int ActivitiesFieldNumber = 1; 332 | private static readonly pb::FieldCodec _repeated_activities_codec 333 | = pb::FieldCodec.ForMessage(10, global::PartialFoods.Services.Activity.Parser); 334 | private readonly pbc::RepeatedField activities_ = new pbc::RepeatedField(); 335 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 336 | public pbc::RepeatedField Activities { 337 | get { return activities_; } 338 | } 339 | 340 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 341 | public override bool Equals(object other) { 342 | return Equals(other as ActivityResponse); 343 | } 344 | 345 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 346 | public bool Equals(ActivityResponse other) { 347 | if (ReferenceEquals(other, null)) { 348 | return false; 349 | } 350 | if (ReferenceEquals(other, this)) { 351 | return true; 352 | } 353 | if(!activities_.Equals(other.activities_)) return false; 354 | return true; 355 | } 356 | 357 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 358 | public override int GetHashCode() { 359 | int hash = 1; 360 | hash ^= activities_.GetHashCode(); 361 | return hash; 362 | } 363 | 364 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 365 | public override string ToString() { 366 | return pb::JsonFormatter.ToDiagnosticString(this); 367 | } 368 | 369 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 370 | public void WriteTo(pb::CodedOutputStream output) { 371 | activities_.WriteTo(output, _repeated_activities_codec); 372 | } 373 | 374 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 375 | public int CalculateSize() { 376 | int size = 0; 377 | size += activities_.CalculateSize(_repeated_activities_codec); 378 | return size; 379 | } 380 | 381 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 382 | public void MergeFrom(ActivityResponse other) { 383 | if (other == null) { 384 | return; 385 | } 386 | activities_.Add(other.activities_); 387 | } 388 | 389 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 390 | public void MergeFrom(pb::CodedInputStream input) { 391 | uint tag; 392 | while ((tag = input.ReadTag()) != 0) { 393 | switch(tag) { 394 | default: 395 | input.SkipLastField(); 396 | break; 397 | case 10: { 398 | activities_.AddEntriesFrom(input, _repeated_activities_codec); 399 | break; 400 | } 401 | } 402 | } 403 | } 404 | 405 | } 406 | 407 | public sealed partial class Activity : pb::IMessage { 408 | private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Activity()); 409 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 410 | public static pb::MessageParser Parser { get { return _parser; } } 411 | 412 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 413 | public static pbr::MessageDescriptor Descriptor { 414 | get { return global::PartialFoods.Services.InventoryReflection.Descriptor.MessageTypes[3]; } 415 | } 416 | 417 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 418 | pbr::MessageDescriptor pb::IMessage.Descriptor { 419 | get { return Descriptor; } 420 | } 421 | 422 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 423 | public Activity() { 424 | OnConstruction(); 425 | } 426 | 427 | partial void OnConstruction(); 428 | 429 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 430 | public Activity(Activity other) : this() { 431 | sKU_ = other.sKU_; 432 | timestamp_ = other.timestamp_; 433 | quantity_ = other.quantity_; 434 | activityType_ = other.activityType_; 435 | activityID_ = other.activityID_; 436 | orderID_ = other.orderID_; 437 | } 438 | 439 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 440 | public Activity Clone() { 441 | return new Activity(this); 442 | } 443 | 444 | /// Field number for the "SKU" field. 445 | public const int SKUFieldNumber = 1; 446 | private string sKU_ = ""; 447 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 448 | public string SKU { 449 | get { return sKU_; } 450 | set { 451 | sKU_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); 452 | } 453 | } 454 | 455 | /// Field number for the "Timestamp" field. 456 | public const int TimestampFieldNumber = 2; 457 | private ulong timestamp_; 458 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 459 | public ulong Timestamp { 460 | get { return timestamp_; } 461 | set { 462 | timestamp_ = value; 463 | } 464 | } 465 | 466 | /// Field number for the "Quantity" field. 467 | public const int QuantityFieldNumber = 3; 468 | private uint quantity_; 469 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 470 | public uint Quantity { 471 | get { return quantity_; } 472 | set { 473 | quantity_ = value; 474 | } 475 | } 476 | 477 | /// Field number for the "ActivityType" field. 478 | public const int ActivityTypeFieldNumber = 4; 479 | private global::PartialFoods.Services.ActivityType activityType_ = 0; 480 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 481 | public global::PartialFoods.Services.ActivityType ActivityType { 482 | get { return activityType_; } 483 | set { 484 | activityType_ = value; 485 | } 486 | } 487 | 488 | /// Field number for the "ActivityID" field. 489 | public const int ActivityIDFieldNumber = 5; 490 | private string activityID_ = ""; 491 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 492 | public string ActivityID { 493 | get { return activityID_; } 494 | set { 495 | activityID_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); 496 | } 497 | } 498 | 499 | /// Field number for the "OrderID" field. 500 | public const int OrderIDFieldNumber = 6; 501 | private string orderID_ = ""; 502 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 503 | public string OrderID { 504 | get { return orderID_; } 505 | set { 506 | orderID_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); 507 | } 508 | } 509 | 510 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 511 | public override bool Equals(object other) { 512 | return Equals(other as Activity); 513 | } 514 | 515 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 516 | public bool Equals(Activity other) { 517 | if (ReferenceEquals(other, null)) { 518 | return false; 519 | } 520 | if (ReferenceEquals(other, this)) { 521 | return true; 522 | } 523 | if (SKU != other.SKU) return false; 524 | if (Timestamp != other.Timestamp) return false; 525 | if (Quantity != other.Quantity) return false; 526 | if (ActivityType != other.ActivityType) return false; 527 | if (ActivityID != other.ActivityID) return false; 528 | if (OrderID != other.OrderID) return false; 529 | return true; 530 | } 531 | 532 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 533 | public override int GetHashCode() { 534 | int hash = 1; 535 | if (SKU.Length != 0) hash ^= SKU.GetHashCode(); 536 | if (Timestamp != 0UL) hash ^= Timestamp.GetHashCode(); 537 | if (Quantity != 0) hash ^= Quantity.GetHashCode(); 538 | if (ActivityType != 0) hash ^= ActivityType.GetHashCode(); 539 | if (ActivityID.Length != 0) hash ^= ActivityID.GetHashCode(); 540 | if (OrderID.Length != 0) hash ^= OrderID.GetHashCode(); 541 | return hash; 542 | } 543 | 544 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 545 | public override string ToString() { 546 | return pb::JsonFormatter.ToDiagnosticString(this); 547 | } 548 | 549 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 550 | public void WriteTo(pb::CodedOutputStream output) { 551 | if (SKU.Length != 0) { 552 | output.WriteRawTag(10); 553 | output.WriteString(SKU); 554 | } 555 | if (Timestamp != 0UL) { 556 | output.WriteRawTag(16); 557 | output.WriteUInt64(Timestamp); 558 | } 559 | if (Quantity != 0) { 560 | output.WriteRawTag(24); 561 | output.WriteUInt32(Quantity); 562 | } 563 | if (ActivityType != 0) { 564 | output.WriteRawTag(32); 565 | output.WriteEnum((int) ActivityType); 566 | } 567 | if (ActivityID.Length != 0) { 568 | output.WriteRawTag(42); 569 | output.WriteString(ActivityID); 570 | } 571 | if (OrderID.Length != 0) { 572 | output.WriteRawTag(50); 573 | output.WriteString(OrderID); 574 | } 575 | } 576 | 577 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 578 | public int CalculateSize() { 579 | int size = 0; 580 | if (SKU.Length != 0) { 581 | size += 1 + pb::CodedOutputStream.ComputeStringSize(SKU); 582 | } 583 | if (Timestamp != 0UL) { 584 | size += 1 + pb::CodedOutputStream.ComputeUInt64Size(Timestamp); 585 | } 586 | if (Quantity != 0) { 587 | size += 1 + pb::CodedOutputStream.ComputeUInt32Size(Quantity); 588 | } 589 | if (ActivityType != 0) { 590 | size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) ActivityType); 591 | } 592 | if (ActivityID.Length != 0) { 593 | size += 1 + pb::CodedOutputStream.ComputeStringSize(ActivityID); 594 | } 595 | if (OrderID.Length != 0) { 596 | size += 1 + pb::CodedOutputStream.ComputeStringSize(OrderID); 597 | } 598 | return size; 599 | } 600 | 601 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 602 | public void MergeFrom(Activity other) { 603 | if (other == null) { 604 | return; 605 | } 606 | if (other.SKU.Length != 0) { 607 | SKU = other.SKU; 608 | } 609 | if (other.Timestamp != 0UL) { 610 | Timestamp = other.Timestamp; 611 | } 612 | if (other.Quantity != 0) { 613 | Quantity = other.Quantity; 614 | } 615 | if (other.ActivityType != 0) { 616 | ActivityType = other.ActivityType; 617 | } 618 | if (other.ActivityID.Length != 0) { 619 | ActivityID = other.ActivityID; 620 | } 621 | if (other.OrderID.Length != 0) { 622 | OrderID = other.OrderID; 623 | } 624 | } 625 | 626 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute] 627 | public void MergeFrom(pb::CodedInputStream input) { 628 | uint tag; 629 | while ((tag = input.ReadTag()) != 0) { 630 | switch(tag) { 631 | default: 632 | input.SkipLastField(); 633 | break; 634 | case 10: { 635 | SKU = input.ReadString(); 636 | break; 637 | } 638 | case 16: { 639 | Timestamp = input.ReadUInt64(); 640 | break; 641 | } 642 | case 24: { 643 | Quantity = input.ReadUInt32(); 644 | break; 645 | } 646 | case 32: { 647 | activityType_ = (global::PartialFoods.Services.ActivityType) input.ReadEnum(); 648 | break; 649 | } 650 | case 42: { 651 | ActivityID = input.ReadString(); 652 | break; 653 | } 654 | case 50: { 655 | OrderID = input.ReadString(); 656 | break; 657 | } 658 | } 659 | } 660 | } 661 | 662 | } 663 | 664 | #endregion 665 | 666 | } 667 | 668 | #endregion Designer generated code 669 | -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/RPC/InventoryGrpc.cs: -------------------------------------------------------------------------------- 1 | // Generated by the protocol buffer compiler. DO NOT EDIT! 2 | // source: inventory.proto 3 | #pragma warning disable 1591 4 | #region Designer generated code 5 | 6 | using System; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using grpc = global::Grpc.Core; 10 | 11 | namespace PartialFoods.Services { 12 | public static partial class InventoryManagement 13 | { 14 | static readonly string __ServiceName = "PartialFoods.Services.InventoryManagement"; 15 | 16 | static readonly grpc::Marshaller __Marshaller_GetProductRequest = grpc::Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), global::PartialFoods.Services.GetProductRequest.Parser.ParseFrom); 17 | static readonly grpc::Marshaller __Marshaller_GetQuantityResponse = grpc::Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), global::PartialFoods.Services.GetQuantityResponse.Parser.ParseFrom); 18 | static readonly grpc::Marshaller __Marshaller_ActivityResponse = grpc::Marshallers.Create((arg) => global::Google.Protobuf.MessageExtensions.ToByteArray(arg), global::PartialFoods.Services.ActivityResponse.Parser.ParseFrom); 19 | 20 | static readonly grpc::Method __Method_GetEffectiveQuantity = new grpc::Method( 21 | grpc::MethodType.Unary, 22 | __ServiceName, 23 | "GetEffectiveQuantity", 24 | __Marshaller_GetProductRequest, 25 | __Marshaller_GetQuantityResponse); 26 | 27 | static readonly grpc::Method __Method_GetActivity = new grpc::Method( 28 | grpc::MethodType.Unary, 29 | __ServiceName, 30 | "GetActivity", 31 | __Marshaller_GetProductRequest, 32 | __Marshaller_ActivityResponse); 33 | 34 | /// Service descriptor 35 | public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor 36 | { 37 | get { return global::PartialFoods.Services.InventoryReflection.Descriptor.Services[0]; } 38 | } 39 | 40 | /// Base class for server-side implementations of InventoryManagement 41 | public abstract partial class InventoryManagementBase 42 | { 43 | public virtual global::System.Threading.Tasks.Task GetEffectiveQuantity(global::PartialFoods.Services.GetProductRequest request, grpc::ServerCallContext context) 44 | { 45 | throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); 46 | } 47 | 48 | public virtual global::System.Threading.Tasks.Task GetActivity(global::PartialFoods.Services.GetProductRequest request, grpc::ServerCallContext context) 49 | { 50 | throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); 51 | } 52 | 53 | } 54 | 55 | /// Client for InventoryManagement 56 | public partial class InventoryManagementClient : grpc::ClientBase 57 | { 58 | /// Creates a new client for InventoryManagement 59 | /// The channel to use to make remote calls. 60 | public InventoryManagementClient(grpc::Channel channel) : base(channel) 61 | { 62 | } 63 | /// Creates a new client for InventoryManagement that uses a custom CallInvoker. 64 | /// The callInvoker to use to make remote calls. 65 | public InventoryManagementClient(grpc::CallInvoker callInvoker) : base(callInvoker) 66 | { 67 | } 68 | /// Protected parameterless constructor to allow creation of test doubles. 69 | protected InventoryManagementClient() : base() 70 | { 71 | } 72 | /// Protected constructor to allow creation of configured clients. 73 | /// The client configuration. 74 | protected InventoryManagementClient(ClientBaseConfiguration configuration) : base(configuration) 75 | { 76 | } 77 | 78 | public virtual global::PartialFoods.Services.GetQuantityResponse GetEffectiveQuantity(global::PartialFoods.Services.GetProductRequest request, grpc::Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken)) 79 | { 80 | return GetEffectiveQuantity(request, new grpc::CallOptions(headers, deadline, cancellationToken)); 81 | } 82 | public virtual global::PartialFoods.Services.GetQuantityResponse GetEffectiveQuantity(global::PartialFoods.Services.GetProductRequest request, grpc::CallOptions options) 83 | { 84 | return CallInvoker.BlockingUnaryCall(__Method_GetEffectiveQuantity, null, options, request); 85 | } 86 | public virtual grpc::AsyncUnaryCall GetEffectiveQuantityAsync(global::PartialFoods.Services.GetProductRequest request, grpc::Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken)) 87 | { 88 | return GetEffectiveQuantityAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken)); 89 | } 90 | public virtual grpc::AsyncUnaryCall GetEffectiveQuantityAsync(global::PartialFoods.Services.GetProductRequest request, grpc::CallOptions options) 91 | { 92 | return CallInvoker.AsyncUnaryCall(__Method_GetEffectiveQuantity, null, options, request); 93 | } 94 | public virtual global::PartialFoods.Services.ActivityResponse GetActivity(global::PartialFoods.Services.GetProductRequest request, grpc::Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken)) 95 | { 96 | return GetActivity(request, new grpc::CallOptions(headers, deadline, cancellationToken)); 97 | } 98 | public virtual global::PartialFoods.Services.ActivityResponse GetActivity(global::PartialFoods.Services.GetProductRequest request, grpc::CallOptions options) 99 | { 100 | return CallInvoker.BlockingUnaryCall(__Method_GetActivity, null, options, request); 101 | } 102 | public virtual grpc::AsyncUnaryCall GetActivityAsync(global::PartialFoods.Services.GetProductRequest request, grpc::Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default(CancellationToken)) 103 | { 104 | return GetActivityAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken)); 105 | } 106 | public virtual grpc::AsyncUnaryCall GetActivityAsync(global::PartialFoods.Services.GetProductRequest request, grpc::CallOptions options) 107 | { 108 | return CallInvoker.AsyncUnaryCall(__Method_GetActivity, null, options, request); 109 | } 110 | /// Creates a new instance of client from given ClientBaseConfiguration. 111 | protected override InventoryManagementClient NewInstance(ClientBaseConfiguration configuration) 112 | { 113 | return new InventoryManagementClient(configuration); 114 | } 115 | } 116 | 117 | /// Creates service definition that can be registered with a server 118 | /// An object implementing the server-side handling logic. 119 | public static grpc::ServerServiceDefinition BindService(InventoryManagementBase serviceImpl) 120 | { 121 | return grpc::ServerServiceDefinition.CreateBuilder() 122 | .AddMethod(__Method_GetEffectiveQuantity, serviceImpl.GetEffectiveQuantity) 123 | .AddMethod(__Method_GetActivity, serviceImpl.GetActivity).Build(); 124 | } 125 | 126 | } 127 | } 128 | #endregion 129 | -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "service": { 3 | "port": "8082" 4 | }, 5 | "kafkaclient": { 6 | "brokerlist": "localhost:9092" 7 | }, 8 | "ConnectionStrings": { 9 | "inventory": "Host=localhost;Port=5432;Database=inventory;Username=postgres;Password=wopr" 10 | } 11 | } -------------------------------------------------------------------------------- /src/PartialFoods.Services.InventoryServer/makeprotos.sh: -------------------------------------------------------------------------------- 1 | PROJDIR=`pwd` 2 | cd ~/.nuget/packages/grpc.tools/1.6.1/tools/linux_x64 3 | protoc -I $PROJDIR/../../proto --csharp_out $PROJDIR/RPC --grpc_out $PROJDIR/RPC $PROJDIR/../../proto/inventory.proto --plugin=protoc-gen-grpc=grpc_csharp_plugin 4 | cd - -------------------------------------------------------------------------------- /tests/PartialFoods.Services.InventoryServer.Tests/PartialFoods.Services.InventoryServer.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/PartialFoods.Services.InventoryServer.Tests/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace PartialFoods.Services.InventoryServer.Tests 5 | { 6 | public class UnitTest1 7 | { 8 | [Fact] 9 | public void Test1() 10 | { 11 | 12 | } 13 | } 14 | } 15 | --------------------------------------------------------------------------------