This document describe how to create a service with Microdot, step-by-step. We will create simple service called "InventoryService". This guide is written for Visual Studio 2017 with projects targeted at .NET Framework 4.7.
InventoryService is an extremely simple service used to manage inventory for products. You can ship items, which decrease the current stock, and you can restock, which increase the current stock.
Class diagram describing the main classes in the solution, to what project they belong and what attributes are applied to each.
Grain
, as required by Orleans), which in turn includes two interfaces: IGrainWithIntegerKey
and the service interface. Other grains can be implements as desired.Paket is an alternative to Microsoft's NuGet Package Manager which makes managing NuGets much easier. It allows specifying packages at the solution level instead of the project level, which keeps version synchronized (no more package consolidation).
.paket
folder under your solution folder that contains paket.exe
.paket.dependencies
file in the root of your solution. The is the solution-wide equivalent of packages.config
- you will specify any package you consume here. You can add this file to your solution so you can easily edit it from within Visual Studio.paket.dependencies
file:source https://www.nuget.org/api/v2/
framework: auto-detect
nuget Gigya.ServiceContract ~> 2.0
nuget Gigya.Microdot.Interfaces ~> 1.0
nuget Gigya.Microdot.Orleans.Hosting ~> 1.0
nuget Gigya.Microdot.Orleans.Ninject.Host ~> 1.0
nuget Gigya.Microdot.Logging.NLog ~> 1.0
nuget Gigya.Microdot.Ninject ~> 1.0
This tells Paket we're using six Microdot NuGets in this solution, limiting their version to specific major versions (e.g. ~> 1.0
is equivalent to the NuGet version range of [1.0,2.0)
) and two Orleans NuGets with their versions not specified (will be constrained by the Microdot NuGet dependency requirements). When using paket update
command, Paket will update the packages to the highest non-prerelease versions that have a major version of 1. We don't want other major versions because they can contain breaking changes (as per Semantic Versioning).
The service interface defines what operations your service provides to its callers. It is put in its own project so it can be published as a NuGet package, which can then be consumed by others, allowing them to call the service using the ServiceProxy. In this example, it contains two simple methods - one used for shipping items out of inventory and one for restocking it.
InventoryService.Interface
.paket.references
with a single line: Gigya.ServiceContract
. This instructs Paket to add the Gigya.ServiceContract
NuGet package to this project.paket.template
with a single line: type project
. This instructs Paket that this project should be packaged as a NuGet.paket.exe install
(in the .paket
folder) to download and install the Gigya.ServiceContract
in the project.IInventoryService.cs
with the following content:using System; using System.Threading.Tasks; using Gigya.Common.Contracts.HttpService; namespace InventoryService.Interface { [HttpService(10000)] public interface IInventoryService { Task ShipItems(Product product, int quantity); Task RestockItems(Product product, int quantity); } public class Product { public Guid Id { get; set; } public string Name { get; set; } } }
OutOfStockException.cs
with the following content:using Gigya.Common.Contracts.Exceptions; using System; using System.Runtime.Serialization; namespace InventoryService.Interface { [Serializable] public class OutOfStockException : RequestException { public OutOfStockException(string message) : base(message) { } protected OutOfStockException(SerializationInfo info, StreamingContext context) : base(info, context) { } } }
See the documentation of SerializableException
for guidelines on how to properly create an exception for use in Microdot.
Your service interface must:
Task
or Task<T>
.HttpService
attribute that contains its default base port.This project will contains grains and their interfaces. This includes the Service Grain and other grains. Orleans generates serialization and client code during build time and runtime, so two additional NuGets are required to support it.
InventoryService.Grains
.paket.references
with a the following:Microsoft.Orleans.OrleansCodeGenerator
Microsoft.Orleans.OrleansCodeGenerator.Build
Gigya.Microdot.Interfaces
Gigya.ServiceContract
paket.exe install
(in the .paket
folder).ProductGrain.cs
with the following content:using Gigya.Common.Contracts.Exceptions; using InventoryService.Interface; using Orleans; using System.Threading.Tasks; namespace InventoryService.Grains { public interface IProductGrain : IGrainWithGuidKey { Task<int> GetCurrentStock(); Task ModifyStock(int quantity); } public class ProductGrain : Grain, IProductGrain { private int CurrentStock { get; set; } public Task<int> GetCurrentStock() { return Task.FromResult(CurrentStock); } public Task ModifyStock(int quantity) { var updatedStock = CurrentStock + quantity; if (updatedStock < 0) throw new OutOfStockException($"Not enough stock to complete the operation. Only {CurrentStock} items in stock."); if (updatedStock > 1000) throw new RequestException($"Cannot add stock - operation will cause the stock to exceed maximum of 1000 by {updatedStock - 1000}."); CurrentStock = updatedStock; if (updatedStock < 5) { // TODO: Send low stock warning -or- order more stock. } return Task.CompletedTask; } } }
InventoryServiceGrain.cs
with the following content:using Gigya.Microdot.Interfaces.Logging; using InventoryService.Interface; using Orleans; using Orleans.Concurrency; using System; using System.Threading.Tasks; namespace InventoryService.Grains { public interface IInventoryServiceGrain : IInventoryService, IGrainWithIntegerKey { } [Reentrant] [StatelessWorker] public class InventoryServiceGrain : Grain, IInventoryServiceGrain { private ILog Log { get; } public InventoryServiceGrain(ILog log) { Log = log; } public async Task RestockItems(Product product, int quantity) { if (quantity < 1) throw new ArgumentOutOfRangeException(nameof(quantity), "Restock quantity must be greater than 0."); var grain = GrainFactory.GetGrain<IProductGrain>(product.Id); await grain.ModifyStock(quantity); Log.Info(_ => _("Product successfully restocked", unencryptedTags: new { product.Name, product.Id, quantity })); } public async Task ShipItems(Product product, int quantity) { if (quantity < 1) throw new ArgumentOutOfRangeException(nameof(quantity), "Ship quantity must be greater than 0."); var grain = GrainFactory.GetGrain<IProductGrain>(product.Id); await grain.ModifyStock(-quantity); Log.Info(_ => _("Product successfully shipped", unencryptedTags: new { product.Name, product.Id, quantity })); // TODO: Send notification to customer that item has shipped } } }
The main project will be the executable and contain your Microdot host and Dependency Injection configuration (Ninject Bindings).
InventoryService
.paket.references
with a the following:Gigya.Microdot.Orleans.Ninject.Host
Gigya.Microdot.Logging.NLog;
paket.exe install
(in the .paket
folder).YourUsernameHere
with your Windows account name (including domain name if applicable):netsh http add urlacl url=http://+:10000/ user=YourUsernameHere
app.config
file to contain the following:<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog"/> </configSections> <nlog throwExceptions="true" throwConfigExceptions="true" xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <targets> <target name="Console" xsi:type="ColoredConsole" layout="${time} ${pad:padding=-5:inner=${level:uppercase=true}} ${message}"/> <target name="Null" xsi:type="Null"/> </targets> <rules> <logger name="Gigya.Microdot.Orleans.Hosting.Logging.OrleansLogConsumer" maxlevel="Info" writeTo="Null" final="true" /> <logger name="*" minlevel="Info" writeTo="Console" /> </rules> </nlog> <runtime> <gcServer enabled="true" /> <gcConcurrent enabled="false" /> </runtime> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7" /> </startup> </configuration>
Add an text file named loadPaths.json
to the project with a content of one line: [ { "Pattern": ".\\*.config", "Priority": 1 } ]
. Set its Copy to Output Directory property to Copy if newer. This file is required by the configuration system, and is used to specify paths to folders containing configuration files.
Add a C# class file named InventoryServiceHost.cs
. It will contain the host of your service. Add the following content:
using Gigya.Microdot.Orleans.Ninject.Host; using Gigya.Microdot.Ninject; using Gigya.Microdot.Logging.NLog; namespace InventoryService { public class InventoryServiceHost : MicrodotOrleansServiceHost { public override ILoggingModule GetLoggingModule() => new NLogModule(); } }
using System; namespace InventoryService { class Program { static void Main(string[] args) { Environment.SetEnvironmentVariable("GIGYA_CONFIG_ROOT", Environment.CurrentDirectory); new InventoryServiceHost().Run(); } } }
Service initialized in interactive mode (command line). Press [Alt+S] to stop the service gracefully.
In order to see the service running, we have to call it somehow. We'll create a simple client that uses ServiceProxy to call the methods of the service.
InventoryService.Client
.paket.references
with a the following:Gigya.Microdot.Interfaces
Gigya.Microdot.Ninject
Gigya.Microdot.Logging.NLog
paket.exe install
(in the .paket
folder).App.config
and loadPaths.json
(with "Copy if newer"), having the same content as in the main project.Discovery.config
. Set it to "Copy if newer". Add the following content:<?xml version="1.0" encoding="utf-8" ?> <configuration> <Discovery> <Services> <InventoryService Source="Local" /> </Services> </Discovery> </configuration>
FortuneCookieTrader.cs
with the following content:using Gigya.Microdot.Interfaces.Logging; using InventoryService.Interface; using System; using System.Threading.Tasks; namespace InventoryService.Client { public class FortuneCookieTrader { private IInventoryService Inventory { get; } private ILog Log { get; } private Product Cookie { get; } public FortuneCookieTrader(IInventoryService inventory, ILog log) { Inventory = inventory; Log = log; var productId = Guid.NewGuid(); var cookieNumber = BitConverter.ToUInt16(productId.ToByteArray(), 0); Cookie = new Product { Id = productId, Name = $"Fortune Cookie #{cookieNumber}" }; } public async Task Start() { while (true) { try { await Inventory.ShipItems(Cookie, 3); Log.Info(_ => _("Shipped three fortune cookies.", unencryptedTags: new { Cookie.Name })); await Task.Delay(1000); } catch (OutOfStockException) { Log.Error("Out of stock! Restocking 10 items."); await Inventory.RestockItems(Cookie, 10); await Task.Delay(5000); } } } } }
Program.cs
to contain the following:using Gigya.Microdot.Logging.NLog; using Gigya.Microdot.Ninject; using Gigya.Microdot.SharedLogic; using Ninject; using System; namespace InventoryService.Client { class Program { static void Main(string[] args) { Environment.SetEnvironmentVariable("GIGYA_CONFIG_ROOT", Environment.CurrentDirectory); CurrentApplicationInfo.Init("InventoryService.Client"); var kernel = new StandardKernel(); kernel.Load<MicrodotModule>(); kernel.Load<NLogModule>(); var trader = kernel.Get<FortuneCookieTrader>(); trader.Start().Wait(); } } }
The finished result of this guide can be found at:
https://github.com/gigya/microdot-samples/tree/master/InventoryService
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4