Add command pattern
This commit is contained in:
@@ -14,6 +14,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "BehavioralPatterns", "src\B
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ChainOfResponssibility", "src\ChainOfResponssibility\ChainOfResponssibility.xproj", "{89536824-683F-4351-8789-406D7BDD922D}"
|
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ChainOfResponssibility", "src\ChainOfResponssibility\ChainOfResponssibility.xproj", "{89536824-683F-4351-8789-406D7BDD922D}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CommandPattern", "src\CommandPattern\CommandPattern.xproj", "{454B2A43-8251-4667-8DE3-67E489908DB9}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -28,6 +30,10 @@ Global
|
|||||||
{89536824-683F-4351-8789-406D7BDD922D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{89536824-683F-4351-8789-406D7BDD922D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{89536824-683F-4351-8789-406D7BDD922D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{89536824-683F-4351-8789-406D7BDD922D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{89536824-683F-4351-8789-406D7BDD922D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{89536824-683F-4351-8789-406D7BDD922D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{454B2A43-8251-4667-8DE3-67E489908DB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{454B2A43-8251-4667-8DE3-67E489908DB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{454B2A43-8251-4667-8DE3-67E489908DB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{454B2A43-8251-4667-8DE3-67E489908DB9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -35,5 +41,6 @@ Global
|
|||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{E3092EE0-1282-4AB4-9FA2-0338348D8FD1} = {3820200F-354C-41E6-8F34-B301F5D621C2}
|
{E3092EE0-1282-4AB4-9FA2-0338348D8FD1} = {3820200F-354C-41E6-8F34-B301F5D621C2}
|
||||||
{89536824-683F-4351-8789-406D7BDD922D} = {3820200F-354C-41E6-8F34-B301F5D621C2}
|
{89536824-683F-4351-8789-406D7BDD922D} = {3820200F-354C-41E6-8F34-B301F5D621C2}
|
||||||
|
{454B2A43-8251-4667-8DE3-67E489908DB9} = {3820200F-354C-41E6-8F34-B301F5D621C2}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using ChainOfResponssibility;
|
using ChainOfResponssibility;
|
||||||
using ChainOfResponssibility.PurchaseExample;
|
using ChainOfResponssibility.PurchaseExample;
|
||||||
|
using CommandPattern;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@@ -16,6 +17,8 @@ namespace BehavioralPatterns
|
|||||||
//This is usefull when you have a request and you don't know who should process it
|
//This is usefull when you have a request and you don't know who should process it
|
||||||
ChainOfResponsibillityExamples.Run();
|
ChainOfResponsibillityExamples.Run();
|
||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
|
CommandPatternExamples.Run();
|
||||||
|
Console.ReadKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ChainOfResponssibility": "1.0.0-*",
|
"ChainOfResponssibility": "1.0.0-*",
|
||||||
|
"CommandPattern": "1.0.0-*",
|
||||||
"Microsoft.NETCore.App": {
|
"Microsoft.NETCore.App": {
|
||||||
"type": "platform",
|
"type": "platform",
|
||||||
"version": "1.0.0-rc2-3002702"
|
"version": "1.0.0-rc2-3002702"
|
||||||
|
|||||||
21
src/CommandPattern/CommandPattern.xproj
Normal file
21
src/CommandPattern/CommandPattern.xproj
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||||
|
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
<PropertyGroup Label="Globals">
|
||||||
|
<ProjectGuid>454b2a43-8251-4667-8de3-67e489908db9</ProjectGuid>
|
||||||
|
<RootNamespace>CommandPattern</RootNamespace>
|
||||||
|
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||||
|
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||||
|
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
|
</Project>
|
||||||
45
src/CommandPattern/CommandPatternExamples.cs
Normal file
45
src/CommandPattern/CommandPatternExamples.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using CommandPattern.StocksExample;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CommandPattern
|
||||||
|
{
|
||||||
|
public class CommandPatternExamples
|
||||||
|
{
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
Console.WriteLine(GetPatternDescription());
|
||||||
|
GoToNextStep();
|
||||||
|
|
||||||
|
StockExampleRunner stockExampleRunner = new StockExampleRunner();
|
||||||
|
Console.WriteLine(stockExampleRunner.GetDescriptionOfExample());
|
||||||
|
GoToNextStep();
|
||||||
|
stockExampleRunner.Run();
|
||||||
|
stockExampleRunner.Run();
|
||||||
|
stockExampleRunner.Run();
|
||||||
|
stockExampleRunner.Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void GoToNextStep()
|
||||||
|
{
|
||||||
|
Console.ReadKey();
|
||||||
|
Console.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetPatternDescription()
|
||||||
|
{
|
||||||
|
return @"
|
||||||
|
command pattern is a behavioral design pattern in which an object is used to encapsulate
|
||||||
|
all information needed to perform an action or trigger an event at a later time
|
||||||
|
Uses:
|
||||||
|
1. Macro recording: f all user actions are represented by command objects, a program can record a
|
||||||
|
sequence of actions simply by keeping a list of the command objects as they are executed.
|
||||||
|
2. Undo
|
||||||
|
3. GUI buttons and menu items
|
||||||
|
4. Parallel processing
|
||||||
|
5. Transactional behavior ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/CommandPattern/Program.cs
Normal file
14
src/CommandPattern/Program.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CommandPattern
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/CommandPattern/Properties/AssemblyInfo.cs
Normal file
19
src/CommandPattern/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("Hewlett-Packard Company")]
|
||||||
|
[assembly: AssemblyProduct("CommandPattern")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("454b2a43-8251-4667-8de3-67e489908db9")]
|
||||||
47
src/CommandPattern/StocksExample/Agent.cs
Normal file
47
src/CommandPattern/StocksExample/Agent.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CommandPattern.StocksExample
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invoker. The invoker will invoke the command
|
||||||
|
/// </summary>
|
||||||
|
public class Agent
|
||||||
|
{
|
||||||
|
Stack<Order> ordersNotExecuted;
|
||||||
|
FixedSizedQueue<Order> ordersPlaced;
|
||||||
|
StockSchedule stockSchedule;
|
||||||
|
|
||||||
|
public Agent(StockSchedule stockSchedule)
|
||||||
|
{
|
||||||
|
ordersNotExecuted = new Stack<Order>();
|
||||||
|
ordersPlaced = new FixedSizedQueue<Order>(10);
|
||||||
|
this.stockSchedule = stockSchedule;
|
||||||
|
stockSchedule.StockExchangedOpened += StockSchedule_StockExchangedOpened;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StockSchedule_StockExchangedOpened(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
while (ordersNotExecuted.Any())
|
||||||
|
{
|
||||||
|
PlaceOrder(ordersNotExecuted.Pop());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PlaceOrder(Order order)
|
||||||
|
{
|
||||||
|
if(stockSchedule.IsStockOpen())
|
||||||
|
{
|
||||||
|
order.Execute();
|
||||||
|
ordersPlaced.Enqueue(order);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("Market is not opened, so the order was saved for later");
|
||||||
|
ordersNotExecuted.Push(order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/CommandPattern/StocksExample/Commands/BuyStockOrder.cs
Normal file
24
src/CommandPattern/StocksExample/Commands/BuyStockOrder.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CommandPattern.StocksExample.Commands
|
||||||
|
{
|
||||||
|
//Concrete Command
|
||||||
|
public class BuyStockOrder : Order
|
||||||
|
{
|
||||||
|
StocksAPI stocksAPI;
|
||||||
|
Stock stock;
|
||||||
|
public BuyStockOrder(StocksAPI stocksAPI, Stock stock)
|
||||||
|
{
|
||||||
|
this.stocksAPI = stocksAPI;
|
||||||
|
this.stock = stock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute()
|
||||||
|
{
|
||||||
|
stocksAPI.Buy(stock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/CommandPattern/StocksExample/Commands/SellStockOrder.cs
Normal file
23
src/CommandPattern/StocksExample/Commands/SellStockOrder.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CommandPattern.StocksExample.Commands
|
||||||
|
{
|
||||||
|
public class SellStockOrder : Order
|
||||||
|
{
|
||||||
|
StocksAPI stocksAPI;
|
||||||
|
Stock stock;
|
||||||
|
public SellStockOrder(StocksAPI stocksAPI, Stock stock)
|
||||||
|
{
|
||||||
|
this.stocksAPI = stocksAPI;
|
||||||
|
this.stock = stock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute()
|
||||||
|
{
|
||||||
|
stocksAPI.Sell(stock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/CommandPattern/StocksExample/FixedSizedQueue.cs
Normal file
29
src/CommandPattern/StocksExample/FixedSizedQueue.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace CommandPattern.StocksExample
|
||||||
|
{
|
||||||
|
public class FixedSizedQueue<T> : ConcurrentQueue<T>
|
||||||
|
{
|
||||||
|
private readonly object syncObject = new object();
|
||||||
|
|
||||||
|
public int Size { get; private set; }
|
||||||
|
|
||||||
|
public FixedSizedQueue(int size)
|
||||||
|
{
|
||||||
|
Size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void Enqueue(T obj)
|
||||||
|
{
|
||||||
|
base.Enqueue(obj);
|
||||||
|
lock (syncObject)
|
||||||
|
{
|
||||||
|
while (Count > Size)
|
||||||
|
{
|
||||||
|
T outObj;
|
||||||
|
TryDequeue(out outObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/CommandPattern/StocksExample/Order.cs
Normal file
15
src/CommandPattern/StocksExample/Order.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CommandPattern.StocksExample
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Command interface
|
||||||
|
/// </summary>
|
||||||
|
public interface Order
|
||||||
|
{
|
||||||
|
void Execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/CommandPattern/StocksExample/Stock.cs
Normal file
8
src/CommandPattern/StocksExample/Stock.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace CommandPattern.StocksExample
|
||||||
|
{
|
||||||
|
public class Stock
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/CommandPattern/StocksExample/StockExampleRunner.cs
Normal file
30
src/CommandPattern/StocksExample/StockExampleRunner.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using CommandPattern.StocksExample.Commands;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CommandPattern.StocksExample
|
||||||
|
{
|
||||||
|
public class StockExampleRunner
|
||||||
|
{
|
||||||
|
public void Run()
|
||||||
|
{
|
||||||
|
StocksAPI stocksAPI = new StocksAPI();
|
||||||
|
Agent agent = new Agent(new StockSchedule());
|
||||||
|
|
||||||
|
Stock stock = new Stock { Name = "AAPL", Quantity = 20 };
|
||||||
|
|
||||||
|
agent.PlaceOrder(new BuyStockOrder(stocksAPI, stock));
|
||||||
|
agent.PlaceOrder(new SellStockOrder(stocksAPI, stock));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetDescriptionOfExample()
|
||||||
|
{
|
||||||
|
return @"
|
||||||
|
Buy or sell a stock on the market.
|
||||||
|
If the market is closed save the orders for when the market opens again.
|
||||||
|
When the market opens place all the orders.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/CommandPattern/StocksExample/StockSchedule.cs
Normal file
50
src/CommandPattern/StocksExample/StockSchedule.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace CommandPattern.StocksExample
|
||||||
|
{
|
||||||
|
public class StockSchedule
|
||||||
|
{
|
||||||
|
TimeSpan openingTime;
|
||||||
|
public StockSchedule()
|
||||||
|
{
|
||||||
|
CheckForOpeningOfStockExchange();
|
||||||
|
openingTime = new TimeSpan(9, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler StockExchangedOpened;
|
||||||
|
|
||||||
|
public bool IsStockOpen()
|
||||||
|
{
|
||||||
|
return new Random().NextDouble() > 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStockExchangedOpened(object state)
|
||||||
|
{
|
||||||
|
StockExchangedOpened?.Invoke(this, null);
|
||||||
|
Thread.Sleep(1);
|
||||||
|
CheckForOpeningOfStockExchange();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckForOpeningOfStockExchange()
|
||||||
|
{
|
||||||
|
var t = new Timer(new TimerCallback(OnStockExchangedOpened), null, Timeout.Infinite, Timeout.Infinite);
|
||||||
|
|
||||||
|
// Figure how much time until opening market
|
||||||
|
DateTime now = DateTime.Now;
|
||||||
|
DateTime openingTime = DateTime.Today.Add(this.openingTime);
|
||||||
|
|
||||||
|
// If it's already past opening time, wait until opening time tomorrow
|
||||||
|
if (now > openingTime)
|
||||||
|
{
|
||||||
|
openingTime = openingTime.AddDays(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int msUntilOpeningHour = (int)((openingTime - now).TotalMilliseconds);
|
||||||
|
|
||||||
|
// Set the timer to elapse only once, at opening time.
|
||||||
|
t.Change(msUntilOpeningHour, Timeout.Infinite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/CommandPattern/StocksExample/StocksAPI.cs
Normal file
20
src/CommandPattern/StocksExample/StocksAPI.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CommandPattern.StocksExample
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Receiver of the command. Concrete command is executing code from this class
|
||||||
|
/// </summary>
|
||||||
|
public class StocksAPI
|
||||||
|
{
|
||||||
|
public void Buy(Stock stock)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Stock [ Name: {0}, Quantity: {1} bought", stock.Name, stock.Quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Sell(Stock stock)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Stock [ Name: {0}, Quantity: {1} sold", stock.Name, stock.Quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/CommandPattern/project.json
Normal file
19
src/CommandPattern/project.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0-*",
|
||||||
|
"buildOptions": {
|
||||||
|
"emitEntryPoint": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.NETCore.App": {
|
||||||
|
"type": "platform",
|
||||||
|
"version": "1.0.0-rc2-3002702"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"frameworks": {
|
||||||
|
"netcoreapp1.0": {
|
||||||
|
"imports": "dnxcore50"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user