Compare commits
2 Commits
ee57c7a3ca
...
1a16846d7d
Author | SHA1 | Date |
---|---|---|
Mike Nolan | 1a16846d7d | |
Mike Nolan | d07cd21b98 |
|
@ -2,13 +2,14 @@
|
|||
[![Tesses.WebServer Nuget](https://badgen.net/nuget/v/Tesses.WebServer)](https://www.nuget.org/packages/Tesses.WebServer/)
|
||||
![Tesses.WebServer Downloads](https://badgen.net/nuget/dt/Tesses.WebServer)
|
||||
|
||||
|
||||
# License
|
||||
Starting with 1.0.3.9 this library will use GPL-3.0
|
||||
If you can not use GPL either use 1.0.3.8 or use another library
|
||||
|
||||
A TcpListener HTTP Server
|
||||
|
||||
WARNING: use at least version 1.0.4.2 because of security issue with paths
|
||||
> WARNING: use at least version 1.0.4.2 because of security issue with paths
|
||||
|
||||
To make your life easier, install [Tesses.WebServer.EasyServer](https://www.nuget.org/packages/Tesses.WebServer.EasyServer) alongside [Tesses.WebServer](https://www.nuget.org/packages/Tesses.WebServer) and use this code:
|
||||
|
||||
|
@ -50,3 +51,5 @@ server.StartServer(9500); //or any port number
|
|||
- Reverse Proxy (in a seperate library)
|
||||
|
||||
> Note: Range code, POST code and Route Class is not mine its a modified version of the code from ( [dajuric/simple-http](https://github.com/dajuric/simple-http/blob/master/Source/SimpleHTTP/Extensions/Response/ResponseExtensions.PartialStream.cs "dajuric/simple-http"))
|
||||
|
||||
> Note the nuget icon is from [here](https://uxwing.com/http-icon/)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
using Tesses;
|
||||
using System.Drawing;
|
||||
using Tesses;
|
||||
using Tesses.WebServer;
|
||||
using Tesses.WebServer.HtmlLayout;
|
||||
namespace Tesses.WebServer.ConsoleApp
|
||||
{
|
||||
class JsonObj
|
||||
|
@ -8,10 +10,60 @@ namespace Tesses.WebServer.ConsoleApp
|
|||
|
||||
public DateTime Birthday {get;set;}=DateTime.Now;
|
||||
}
|
||||
public class MyOther
|
||||
{
|
||||
[FormNewLine]
|
||||
|
||||
public string Hello {get;set;}="";
|
||||
[FormNewLine]
|
||||
|
||||
public Color FavoriteColor {get;set;}=Color.Pink;
|
||||
// [FormNewLine]
|
||||
|
||||
//public HttpFileResponseEntry[] Files {get;set;}=new HttpFileResponseEntry[0];
|
||||
}
|
||||
public enum TestEnum
|
||||
{
|
||||
Apple,
|
||||
Orange,
|
||||
Grape,
|
||||
Banana,
|
||||
Raspberry,
|
||||
|
||||
Blueberry,
|
||||
Strawberry
|
||||
}
|
||||
class Test
|
||||
{
|
||||
[FormNewLine]
|
||||
[FormText("Your Name",Placeholder="Name",Name="name")]
|
||||
public string Name {get;set;}="";
|
||||
[FormNewLine]
|
||||
[FormText("Describe yourself",Placeholder="Description",Name="description")]
|
||||
public string Description {get;set;}="";
|
||||
[FormNewLine]
|
||||
[FormCheckbox("Are you an adult")]
|
||||
public bool Adult {get;set;}=false;
|
||||
[FormNewLine]
|
||||
[FormCheckbox("Email Me")]
|
||||
public bool EmailMe {get;set;}=true;
|
||||
|
||||
[FormNewLine]
|
||||
[FormRadio("fruit")]
|
||||
public TestEnum Fruit {get;set;}=TestEnum.Raspberry;
|
||||
|
||||
[FormNewLine]
|
||||
[FormFieldSet]
|
||||
public MyOther MyOther {get;set;}=new MyOther();
|
||||
|
||||
|
||||
}
|
||||
|
||||
class MainClass
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
|
||||
TestObject some_object = new TestObject();
|
||||
RouteServer rserver = new RouteServer();
|
||||
rserver.Add("/", async(ctx) => {
|
||||
|
@ -20,6 +72,43 @@ namespace Tesses.WebServer.ConsoleApp
|
|||
rserver.Add("/page", async(ctx) => {
|
||||
await ctx.SendTextAsync("Demetria Devonne Lovato 8/20/1992");
|
||||
});
|
||||
rserver.Add("/john",async(ctx)=>{
|
||||
Test other = new Test();
|
||||
ctx.ParseSmartForm(other);
|
||||
|
||||
await ctx.SendJsonAsync(other);
|
||||
},"POST");
|
||||
rserver.Add("/html_ex",async(ctx)=>{await ctx.SendHtmlAsync(H.Html(
|
||||
H.Head(
|
||||
H.Meta().WithAttribute("charset","UTF-8"),
|
||||
H.Meta().WithAttribute("name","viewport").WithAttribute("content","width=device-width, initial-scale=1.0"),
|
||||
H.Title("Document")
|
||||
),
|
||||
H.Body(H.Form("./john",new Test(),true))
|
||||
).WithAttribute("lang","en"));});
|
||||
rserver.Add("/absolute_paths",(ctx)=>{
|
||||
using(var sw = ctx.GetResponseStreamWriter())
|
||||
{
|
||||
sw.WriteLine($"Root: {ctx.GetRealRootUrl()}");
|
||||
sw.WriteLine($"Current Server Root: {ctx.GetCurrentServerPath()}");
|
||||
sw.WriteLine($"Demetria: {ctx.GetRealUrlRelativeToCurrentServer("/page")}");
|
||||
sw.WriteLine($"Headers: {ctx.GetRealUrlRelativeToCurrentServer("/headers")}");
|
||||
sw.WriteLine($"Relative To Root: {ctx.GetRealUrl("/johnconnor/")}");
|
||||
}
|
||||
});
|
||||
|
||||
rserver.Add("/headers",(ctx)=>{
|
||||
using(var sw = ctx.GetResponseStreamWriter())
|
||||
{
|
||||
foreach(var item in ctx.RequestHeaders)
|
||||
{
|
||||
foreach(var item2 in item.Value)
|
||||
{
|
||||
sw.WriteLine($"{item.Key}: {item2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rserver.Add("/jsonEndpoint",async(ctx)=>{
|
||||
var res=await ctx.ReadJsonAsync<JsonObj>();
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Tesses.WebServer.NetStandard\Tesses.WebServer.NetStandard.csproj" />
|
||||
<ProjectReference Include="..\Tesses.WebServer.HtmlLayout\Tesses.WebServer.HtmlLayout.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.IO;
|
|||
using System;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Tesses.WebServer
|
||||
{
|
||||
|
@ -71,6 +72,17 @@ internal class SizedStream : Stream
|
|||
}
|
||||
public class ServerContext
|
||||
{
|
||||
static Mutex mtx=new Mutex();
|
||||
static long _unique=0;
|
||||
|
||||
public static long UniqueNumber()
|
||||
{
|
||||
mtx.WaitOne();
|
||||
long u=_unique++;
|
||||
mtx.ReleaseMutex();
|
||||
return u;
|
||||
}
|
||||
|
||||
const string bad_chars = "<>?/\\\"*|:";
|
||||
public static string FixFileName(string filename,bool requireAscii=false)
|
||||
{
|
||||
|
|
|
@ -5,9 +5,11 @@ using System.Linq;
|
|||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using HeyRed.Mime;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Tesses.WebServer
|
||||
{
|
||||
|
@ -200,8 +202,7 @@ namespace Tesses.WebServer
|
|||
|
||||
}
|
||||
ctx.ResponseHeaders.Add("Content-Length", (end - start + 1).ToString());
|
||||
ctx.ResponseHeaders.Add("Content-Type", contentType);
|
||||
|
||||
ctx.WithMimeType(contentType);
|
||||
await ctx.WriteHeadersAsync();
|
||||
if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal))
|
||||
{
|
||||
|
@ -537,6 +538,11 @@ namespace Tesses.WebServer
|
|||
}
|
||||
return files;
|
||||
}
|
||||
public static HttpFileResponse ParseBodyWithTempDirectory(this ServerContext request)
|
||||
{
|
||||
DateTime dt=DateTime.Now;
|
||||
return request.ParseBodyWithTempDirectory(Path.Combine(Path.GetTempPath(),$"TWSUPLOAD_{dt.ToString("yyyyMMdd_HHmmss")}_{ServerContext.UniqueNumber()}"));
|
||||
}
|
||||
/// <summary>
|
||||
/// Parses body of the request including form and multi-part form data, allowing multiple file with same key and storing the files in a temp directory specified by the user.
|
||||
/// </summary>
|
||||
|
@ -568,6 +574,7 @@ namespace Tesses.WebServer
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public sealed class HttpFileResponseEntry
|
||||
{
|
||||
public HttpFileResponseEntry(string path, string filename, string fieldname, string contype)
|
||||
|
@ -584,8 +591,10 @@ namespace Tesses.WebServer
|
|||
public string Path {get;}
|
||||
|
||||
public string FieldName {get;}
|
||||
|
||||
[JsonIgnore]
|
||||
public FileInfo FileInfo => new FileInfo(Path);
|
||||
[JsonIgnore]
|
||||
public object PrivateData {get;set;}=null;
|
||||
|
||||
public Stream OpenRead()
|
||||
{
|
||||
|
@ -606,9 +615,15 @@ namespace Tesses.WebServer
|
|||
public IReadOnlyList<HttpFileResponseEntry> Files {get;}
|
||||
public string Directory {get;}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
System.IO.Directory.Delete(Directory,true);
|
||||
if(System.IO.Directory.Exists(Directory))
|
||||
System.IO.Directory.Delete(Directory,true);
|
||||
}
|
||||
~HttpFileResponse()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,14 +5,16 @@
|
|||
<PackageId>Tesses.WebServer</PackageId>
|
||||
<Author>Mike Nolan</Author>
|
||||
<Company>Tesses</Company>
|
||||
<Version>1.0.4.2</Version>
|
||||
<AssemblyVersion>1.0.4.2</AssemblyVersion>
|
||||
<FileVersion>1.0.4.2</FileVersion>
|
||||
<Version>1.0.4.3</Version>
|
||||
<AssemblyVersion>1.0.4.3</AssemblyVersion>
|
||||
<FileVersion>1.0.4.3</FileVersion>
|
||||
<Description>A TCP Listener HTTP(s) Server</Description>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>HTTP, WebServer, Website</PackageTags>
|
||||
<RepositoryUrl>https://gitlab.tesses.net/tesses50/tesses.webserver</RepositoryUrl>
|
||||
<RepositoryUrl>https://gitea.site.tesses.net/tesses50/tesses.webserver</RepositoryUrl>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>http-icon.png</PackageIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -20,5 +22,8 @@
|
|||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Tesses.VirtualFileSystem.Base" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\http-icon.png" Pack="true" PackagePath="\" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -15,6 +15,7 @@ using System.Security.Authentication;
|
|||
using System.Web;
|
||||
using Tesses.VirtualFilesystem;
|
||||
using System.Net.Mime;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Tesses.WebServer
|
||||
{
|
||||
|
@ -43,6 +44,41 @@ namespace Tesses.WebServer
|
|||
|
||||
public static class Extensions
|
||||
{
|
||||
public static string GetRealRootUrl(this ServerContext ctx)
|
||||
{
|
||||
if(ctx.RequestHeaders.TryGetFirst("X-Forwarded-Path",out var xfwp))
|
||||
{
|
||||
return $"{xfwp.TrimEnd('/')}/";
|
||||
}
|
||||
else if(ctx.RequestHeaders.TryGetFirst("X-Forwarded-Host",out var host))
|
||||
{
|
||||
if(!ctx.RequestHeaders.TryGetFirst("X-Forwarded-Proto",out var proto)) proto="http";
|
||||
return $"{proto}://{host}/";
|
||||
}
|
||||
else if(ctx.RequestHeaders.TryGetFirst("Host",out var theHost))
|
||||
{
|
||||
return $"http://{theHost}/";
|
||||
}
|
||||
return $"http://{ctx.Client}/";
|
||||
}
|
||||
public static string GetRealUrl(this ServerContext ctx,string url)
|
||||
{
|
||||
return $"{ctx.GetRealRootUrl()}{url.TrimStart('/')}";
|
||||
}
|
||||
public static string GetCurrentServerPath(this ServerContext ctx)
|
||||
{
|
||||
if(ctx.UrlPath == ctx.OriginalUrlPath) return "/";
|
||||
return $"{ctx.OriginalUrlPath.Remove(ctx.OriginalUrlPath.Length-ctx.UrlPath.Length).TrimEnd('/')}/";
|
||||
}
|
||||
public static string GetRealUrlRelativeToCurrentServer(this ServerContext ctx, string url)
|
||||
{
|
||||
return ctx.GetRealUrl($"{ctx.GetCurrentServerPath()}{url.TrimStart('/')}");
|
||||
}
|
||||
public static ServerContext WithStatusCode(this ServerContext ctx, int statusCode)
|
||||
{
|
||||
ctx.StatusCode = statusCode;
|
||||
return ctx;
|
||||
}
|
||||
public static ServerContext WithStatusCode(this ServerContext ctx, HttpStatusCode statusCode)
|
||||
{
|
||||
ctx.StatusCode = (int)statusCode;
|
||||
|
@ -116,7 +152,11 @@ namespace Tesses.WebServer
|
|||
try{
|
||||
EventHandler<SendEventArgs> cb= (sender,e0)=>{
|
||||
if(__connected)
|
||||
ctx.NetworkStream.Write($"data: {e0.Data}\n\n");
|
||||
{
|
||||
ctx.NetworkStream.Write($"data: {e0.Data}\n\n");
|
||||
ctx.NetworkStream.Flush();
|
||||
}
|
||||
|
||||
};
|
||||
evt.EventReceived += cb;
|
||||
while(ctx.Connected);
|
||||
|
@ -483,6 +523,229 @@ namespace Tesses.WebServer
|
|||
value = default(T2);
|
||||
return false;
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, object value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, int value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, short value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, long value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, Guid value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, byte value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, sbyte value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, uint value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, ushort value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, ulong value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, bool value)
|
||||
{
|
||||
args.Add(key,value ? "true" : "false");
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, float value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, double value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, decimal value)
|
||||
{
|
||||
args.Add(key,value.ToString());
|
||||
}
|
||||
|
||||
public static bool TryGetFirstInt64<T1>(this Dictionary<T1,List<string>> args,T1 key, out long value)
|
||||
{
|
||||
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= long.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=0;
|
||||
return false;
|
||||
}
|
||||
public static bool TryGetFirstInt32<T1>(this Dictionary<T1,List<string>> args,T1 key, out int value)
|
||||
{
|
||||
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= int.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=0;
|
||||
return false;
|
||||
}
|
||||
public static bool TryGetFirstInt16<T1>(this Dictionary<T1,List<string>> args,T1 key, out short value)
|
||||
{
|
||||
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= short.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=0;
|
||||
return false;
|
||||
}
|
||||
public static bool TryGetFirstInt8<T1>(this Dictionary<T1,List<string>> args,T1 key, out sbyte value)
|
||||
{
|
||||
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= sbyte.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=0;
|
||||
return false;
|
||||
}
|
||||
public static bool GetFirstBoolean<T1>(this Dictionary<T1,List<string>> args,T1 key)
|
||||
{
|
||||
if(args.TryGetFirst(key,out var value))
|
||||
{
|
||||
value=value.ToLower();
|
||||
if(value == "off" || value == "no" || value == "false" || value == "0") return false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public static bool TryGetFirstGuid<T1>(this Dictionary<T1,List<string>> args,T1 key, out Guid value)
|
||||
{
|
||||
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= Guid.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
public static bool TryGetFirstUInt8<T1>(this Dictionary<T1,List<string>> args,T1 key, out byte value)
|
||||
{
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= byte.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=0;
|
||||
return false;
|
||||
}
|
||||
public static bool TryGetFirstUInt16<T1>(this Dictionary<T1,List<string>> args,T1 key, out ushort value)
|
||||
{
|
||||
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= ushort.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=0;
|
||||
return false;
|
||||
}
|
||||
public static bool TryGetFirstUInt64<T1>(this Dictionary<T1,List<string>> args,T1 key, out ulong value)
|
||||
{
|
||||
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= ulong.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=0;
|
||||
return false;
|
||||
}
|
||||
public static bool TryGetFirstUInt32<T1>(this Dictionary<T1,List<string>> args,T1 key, out uint value)
|
||||
{
|
||||
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= uint.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetFirstFloat<T1>(this Dictionary<T1,List<string>> args,T1 key, out float value)
|
||||
{
|
||||
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= float.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=0.0f;
|
||||
return false;
|
||||
}
|
||||
public static bool TryGetFirstBigInteger<T1>(this Dictionary<T1,List<string>> args,T1 key, out BigInteger value)
|
||||
{
|
||||
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= BigInteger.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=0;
|
||||
return false;
|
||||
}
|
||||
public static bool TryGetFirstDecimal<T1>(this Dictionary<T1,List<string>> args,T1 key, out decimal value)
|
||||
{
|
||||
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= decimal.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=0.0M;
|
||||
return false;
|
||||
}
|
||||
public static bool TryGetFirstDouble<T1>(this Dictionary<T1,List<string>> args,T1 key, out double value)
|
||||
{
|
||||
|
||||
if(args.TryGetFirst(key,out var str))
|
||||
{
|
||||
bool res= double.TryParse(str,out var val);
|
||||
value = val;
|
||||
return res;
|
||||
}
|
||||
value=0.0;
|
||||
return false;
|
||||
}
|
||||
/// <summary>
|
||||
/// Add item to the Dictionary<T1,List<T2>> with specified key (will create key in dictionary if not exist)
|
||||
/// </summary>
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
Loading…
Reference in New Issue