Refactored container helper service. Cleaned up event models. Implemented version changing. Added security questions before rebuild
This commit is contained in:
@@ -2,19 +2,21 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moonlight.Api.Configuration;
|
using Moonlight.Api.Configuration;
|
||||||
|
using Moonlight.Api.Mappers;
|
||||||
using Moonlight.Api.Services;
|
using Moonlight.Api.Services;
|
||||||
|
using Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
|
||||||
using Moonlight.Shared.Http.Responses.Admin;
|
using Moonlight.Shared.Http.Responses.Admin;
|
||||||
|
|
||||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/admin/ch")]
|
[Route("api/admin/ch")]
|
||||||
public class ChController : Controller
|
public class ContainerHelperController : Controller
|
||||||
{
|
{
|
||||||
private readonly ContainerHelperService ContainerHelperService;
|
private readonly ContainerHelperService ContainerHelperService;
|
||||||
private readonly IOptions<ContainerHelperOptions> Options;
|
private readonly IOptions<ContainerHelperOptions> Options;
|
||||||
|
|
||||||
public ChController(ContainerHelperService containerHelperService, IOptions<ContainerHelperOptions> options)
|
public ContainerHelperController(ContainerHelperService containerHelperService, IOptions<ContainerHelperOptions> options)
|
||||||
{
|
{
|
||||||
ContainerHelperService = containerHelperService;
|
ContainerHelperService = containerHelperService;
|
||||||
Options = options;
|
Options = options;
|
||||||
@@ -35,9 +37,17 @@ public class ChController : Controller
|
|||||||
public Task<IResult> RebuildAsync()
|
public Task<IResult> RebuildAsync()
|
||||||
{
|
{
|
||||||
var result = ContainerHelperService.RebuildAsync();
|
var result = ContainerHelperService.RebuildAsync();
|
||||||
|
var mappedResult = result.Select(ContainerHelperMapper.ToDto);
|
||||||
|
|
||||||
return Task.FromResult<IResult>(
|
return Task.FromResult<IResult>(
|
||||||
TypedResults.ServerSentEvents(result)
|
TypedResults.ServerSentEvents(mappedResult)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("version")]
|
||||||
|
public async Task<ActionResult> SetVersionAsync([FromBody] SetVersionDto request)
|
||||||
|
{
|
||||||
|
await ContainerHelperService.SetVersionAsync(request.Version);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Http.Services.ContainerHelper.Events;
|
||||||
|
|
||||||
|
public struct RebuildEventDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public RebuildEventType Type { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public string Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RebuildEventType
|
||||||
|
{
|
||||||
|
Log = 0,
|
||||||
|
Failed = 1,
|
||||||
|
Succeeded = 2,
|
||||||
|
Step = 3
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Moonlight.Api.Http.Services.ContainerHelper;
|
||||||
|
|
||||||
|
public class ProblemDetails
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public int Status { get; set; }
|
||||||
|
public string? Detail { get; set; }
|
||||||
|
public Dictionary<string, string[]>? Errors { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||||
|
|
||||||
|
public record SetVersionDto(string Version);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Moonlight.Api.Http.Services.ContainerHelper.Events;
|
||||||
|
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Http.Services.ContainerHelper;
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(SetVersionDto))]
|
||||||
|
[JsonSerializable(typeof(ProblemDetails))]
|
||||||
|
[JsonSerializable(typeof(RebuildEventDto))]
|
||||||
|
public partial class SerializationContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
private static JsonSerializerOptions? InternalTunedOptions;
|
||||||
|
|
||||||
|
public static JsonSerializerOptions TunedOptions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (InternalTunedOptions != null)
|
||||||
|
return InternalTunedOptions;
|
||||||
|
|
||||||
|
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||||
|
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
|
||||||
|
|
||||||
|
return InternalTunedOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Moonlight.Api/Mappers/ContainerHelperMapper.cs
Normal file
13
Moonlight.Api/Mappers/ContainerHelperMapper.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Moonlight.Shared.Http.Events;
|
||||||
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Mappers;
|
||||||
|
|
||||||
|
[Mapper]
|
||||||
|
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
|
||||||
|
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
|
||||||
|
public static partial class ContainerHelperMapper
|
||||||
|
{
|
||||||
|
public static partial RebuildEventDto ToDto(Http.Services.ContainerHelper.Events.RebuildEventDto rebuildEventDto);
|
||||||
|
}
|
||||||
@@ -35,4 +35,8 @@
|
|||||||
<Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible>
|
<Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Http\Services\ContainerHelper\Responses\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.Text.Json;
|
using System.Net.Http.Json;
|
||||||
using Moonlight.Shared.Http;
|
using System.Text.Json;
|
||||||
using Moonlight.Shared.Http.Events;
|
using Moonlight.Api.Http.Services.ContainerHelper;
|
||||||
|
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
|
||||||
|
using Moonlight.Api.Http.Services.ContainerHelper.Events;
|
||||||
|
|
||||||
namespace Moonlight.Api.Services;
|
namespace Moonlight.Api.Services;
|
||||||
|
|
||||||
@@ -30,61 +32,73 @@ public class ContainerHelperService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<RebuildEvent> RebuildAsync()
|
public async IAsyncEnumerable<RebuildEventDto> RebuildAsync()
|
||||||
{
|
{
|
||||||
var options = new JsonSerializerOptions()
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
options.TypeInfoResolverChain.Add(SerializationContext.Default);
|
|
||||||
|
|
||||||
var client = HttpClientFactory.CreateClient("ContainerHelper");
|
var client = HttpClientFactory.CreateClient("ContainerHelper");
|
||||||
|
|
||||||
var response = await client.GetAsync("api/rebuild", HttpCompletionOption.ResponseHeadersRead);
|
var response = await client.GetAsync("api/rebuild", HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var responseText = await response.Content.ReadAsStringAsync();
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
yield return new RebuildEvent()
|
yield return new RebuildEventDto()
|
||||||
{
|
{
|
||||||
Type = RebuildEventType.Failed,
|
Type = RebuildEventType.Failed,
|
||||||
Data = responseText
|
Data = responseText
|
||||||
};
|
};
|
||||||
|
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||||
using var streamReader = new StreamReader(responseStream);
|
using var streamReader = new StreamReader(responseStream);
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
var line = await streamReader.ReadLineAsync();
|
var line = await streamReader.ReadLineAsync();
|
||||||
|
|
||||||
if(line == null)
|
if (line == null)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(line))
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var data = line.Trim("data: ");
|
var data = line.Trim("data: ");
|
||||||
|
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.TunedOptions);
|
||||||
var deserializedData = JsonSerializer.Deserialize<RebuildEvent>(data, options);
|
|
||||||
|
|
||||||
yield return deserializedData;
|
yield return deserializedData;
|
||||||
|
|
||||||
// Exit if service will go down for a clean exit
|
// Exit if service will go down for a clean exit
|
||||||
if(deserializedData is {Type: RebuildEventType.Step, Data: "ServiceDown"})
|
if (deserializedData is { Type: RebuildEventType.Step, Data: "ServiceDown" })
|
||||||
yield break;
|
yield break;
|
||||||
|
|
||||||
} while (true);
|
} while (true);
|
||||||
|
|
||||||
yield return new RebuildEvent()
|
yield return new RebuildEventDto()
|
||||||
{
|
{
|
||||||
Type = RebuildEventType.Succeeded,
|
Type = RebuildEventType.Succeeded,
|
||||||
Data = string.Empty
|
Data = string.Empty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetVersionAsync(string version)
|
||||||
|
{
|
||||||
|
var client = HttpClientFactory.CreateClient("ContainerHelper");
|
||||||
|
|
||||||
|
var response = await client.PostAsJsonAsync(
|
||||||
|
"api/configuration/version",
|
||||||
|
new SetVersionDto(version),
|
||||||
|
SerializationContext.TunedOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
if(response.IsSuccessStatusCode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>(SerializationContext.TunedOptions);
|
||||||
|
|
||||||
|
if(problemDetails == null)
|
||||||
|
throw new HttpRequestException($"Failed to set version: {response.ReasonPhrase}");
|
||||||
|
|
||||||
|
throw new HttpRequestException($"Failed to set version: {problemDetails.Detail ?? problemDetails.Title}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ public partial class Startup
|
|||||||
{
|
{
|
||||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SharedSerializationContext.Default);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public static class Constants
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add source generated options from shared project
|
// Add source generated options from shared project
|
||||||
InternalOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
InternalOptions.TypeInfoResolverChain.Add(SharedSerializationContext.Default);
|
||||||
|
|
||||||
return InternalOptions;
|
return InternalOptions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@using System.Text.Json
|
@using System.Text.Json
|
||||||
@using LucideBlazor
|
@using LucideBlazor
|
||||||
@using Moonlight.Shared.Http.Events
|
@using Moonlight.Shared.Http.Events
|
||||||
|
@using Moonlight.Shared.Http.Requests.Admin.ContainerHelper
|
||||||
@using ShadcnBlazor.Buttons
|
@using ShadcnBlazor.Buttons
|
||||||
@using ShadcnBlazor.Dialogs
|
@using ShadcnBlazor.Dialogs
|
||||||
@using ShadcnBlazor.Progresses
|
@using ShadcnBlazor.Progresses
|
||||||
@@ -110,7 +111,10 @@ else
|
|||||||
Progress = 20;
|
Progress = 20;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
await Task.Delay(2000);
|
await HttpClient.PostAsJsonAsync("api/admin/ch/version", new SetVersionDto()
|
||||||
|
{
|
||||||
|
Version = Version
|
||||||
|
});
|
||||||
|
|
||||||
// Starting rebuild task
|
// Starting rebuild task
|
||||||
CurrentStep = 2;
|
CurrentStep = 2;
|
||||||
@@ -138,7 +142,7 @@ else
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
var data = line.Trim("data: ");
|
var data = line.Trim("data: ");
|
||||||
var deserializedData = JsonSerializer.Deserialize<RebuildEvent>(data, Constants.SerializerOptions);
|
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, Constants.SerializerOptions);
|
||||||
|
|
||||||
switch (deserializedData.Type)
|
switch (deserializedData.Type)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@using System.Text.RegularExpressions
|
||||||
@using LucideBlazor
|
@using LucideBlazor
|
||||||
@using Moonlight.Frontend.UI.Admin.Modals
|
@using Moonlight.Frontend.UI.Admin.Modals
|
||||||
@using Moonlight.Shared.Http.Responses.Admin
|
@using Moonlight.Shared.Http.Responses.Admin
|
||||||
@@ -39,14 +40,15 @@
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent ClassName="w-64">
|
<SelectContent ClassName="w-64">
|
||||||
<SelectItem Value="v2.1">v2.1</SelectItem>
|
<SelectItem Value="v2.1">v2.1</SelectItem>
|
||||||
<SelectItem Value="v2.1.1">v2.1.1</SelectItem>
|
<SelectItem Value="feat/ContainerHelper">feat/ContainerHelper
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FieldContent>
|
</FieldContent>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
<Field Orientation="FieldOrientation.Horizontal">
|
<Field Orientation="FieldOrientation.Horizontal">
|
||||||
<Button @onclick="ApplyAsync">Apply</Button>
|
<Button @onclick="AskApplyAsync">Apply</Button>
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -116,20 +118,73 @@
|
|||||||
|
|
||||||
private async Task ApplyAsync()
|
private async Task ApplyAsync()
|
||||||
{
|
{
|
||||||
await AlertDialogService.ConfirmDangerAsync(
|
await DialogService.LaunchAsync<UpdateInstanceModal>(
|
||||||
"Moonlight Rebuild",
|
parameters => { parameters[nameof(UpdateInstanceModal.Version)] = SelectedVersion; },
|
||||||
"If you continue the moonlight instance will become unavailable during the rebuild process. This will impact users on this instance",
|
onConfigure: model =>
|
||||||
async () =>
|
|
||||||
{
|
{
|
||||||
await DialogService.LaunchAsync<UpdateInstanceModal>(
|
model.ShowCloseButton = false;
|
||||||
parameters => { parameters[nameof(UpdateInstanceModal.Version)] = SelectedVersion; },
|
model.ClassName = "sm:max-w-4xl!";
|
||||||
onConfigure: model =>
|
|
||||||
{
|
|
||||||
model.ShowCloseButton = false;
|
|
||||||
model.ClassName = "sm:max-w-4xl!";
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task AskApplyAsync()
|
||||||
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(SelectedVersion))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var shouldContinue = await ConfirmRiskyVersionAsync(
|
||||||
|
"Moonlight Rebuild",
|
||||||
|
"If you continue the moonlight instance will become unavailable during the rebuild process. This will impact users on this instance"
|
||||||
|
);
|
||||||
|
|
||||||
|
if(!shouldContinue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!Regex.IsMatch(SelectedVersion, @"^v\d+(\.\d+)*b?$"))
|
||||||
|
{
|
||||||
|
shouldContinue = await ConfirmRiskyVersionAsync(
|
||||||
|
"Development Version",
|
||||||
|
"You are about to install development a version. This can break your instance. Continue at your own risk"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (SelectedVersion.EndsWith('b'))
|
||||||
|
{
|
||||||
|
shouldContinue = await ConfirmRiskyVersionAsync(
|
||||||
|
"Beta / Pre-Release Version",
|
||||||
|
"You are about to install a version marked as pre-release / beta. This can break your instance. Continue at your own risk"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
shouldContinue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldContinue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await ApplyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ConfirmRiskyVersionAsync(string title, string message)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource();
|
||||||
|
var confirmed = false;
|
||||||
|
|
||||||
|
await AlertDialogService.ConfirmDangerAsync(
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
confirmed = true;
|
||||||
|
tcs.SetResult();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await tcs.Task;
|
||||||
|
|
||||||
|
return confirmed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Moonlight.Shared.Http.Events;
|
namespace Moonlight.Shared.Http.Events;
|
||||||
|
|
||||||
public struct RebuildEvent
|
public struct RebuildEventDto
|
||||||
{
|
{
|
||||||
[JsonPropertyName("type")]
|
[JsonPropertyName("type")]
|
||||||
public RebuildEventType Type { get; set; }
|
public RebuildEventType Type { get; set; }
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
|
||||||
|
|
||||||
|
public class SetVersionDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[RegularExpression(@"^(?!\/|.*\/\/|.*\.\.|.*\/$)[A-Za-z0-9._/-]+$", ErrorMessage = "Invalid version format")]
|
||||||
|
public string Version { get; set; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Moonlight.Shared.Http.Events;
|
using Moonlight.Shared.Http.Events;
|
||||||
using Moonlight.Shared.Http.Requests.ApiKeys;
|
using Moonlight.Shared.Http.Requests.ApiKeys;
|
||||||
using Moonlight.Shared.Http.Requests.Roles;
|
using Moonlight.Shared.Http.Requests.Roles;
|
||||||
@@ -46,13 +47,28 @@ namespace Moonlight.Shared.Http;
|
|||||||
[JsonSerializable(typeof(ThemeDto))]
|
[JsonSerializable(typeof(ThemeDto))]
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
[JsonSerializable(typeof(RebuildEvent))]
|
[JsonSerializable(typeof(RebuildEventDto))]
|
||||||
|
|
||||||
// Container Helper
|
// Container Helper
|
||||||
[JsonSerializable(typeof(ContainerHelperStatusDto))]
|
[JsonSerializable(typeof(ContainerHelperStatusDto))]
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
[JsonSerializable(typeof(ProblemDetails))]
|
[JsonSerializable(typeof(ProblemDetails))]
|
||||||
public partial class SerializationContext : JsonSerializerContext
|
public partial class SharedSerializationContext : JsonSerializerContext
|
||||||
{
|
{
|
||||||
|
private static JsonSerializerOptions? InternalTunedOptions;
|
||||||
|
|
||||||
|
public static JsonSerializerOptions TunedOptions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (InternalTunedOptions != null)
|
||||||
|
return InternalTunedOptions;
|
||||||
|
|
||||||
|
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||||
|
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
|
||||||
|
|
||||||
|
return InternalTunedOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user