Interceptors¶
Ìnterceptor<T>
: A more advanced technic to handle a lot of more events from within DMS
Overview¶
The Progress<T>
stuff is great, but as we said, it’s mainly read only, and the progress is always reported at the end of a current sync stage.
Interceptor<T>
.OnMethodAsync()
method:Imagine you have a table that should never be synchronized on one particular client (and is part of your SyncSetup
). You’re able to use an interceptor like this:
// We are using a cancellation token that will be passed as an argument
// to the SynchronizeAsync() method !
var cts = new CancellationTokenSource();
agent.LocalOrchestrator.OnTableChangesApplying((args) =>
{
if (args.SchemaTable.TableName == "Table_That_Should_Not_Be_Sync")
args.Cancel = true;
});
Be careful, your table will never be synced !
Intercepting rows¶
Hint
You will find the sample used for this chapter, here : Spy sample.
DMS
workload allows you to intecept different kinds of events on different levels:
Database level
Table level
Row level
On each side (client and server), you will have:
Interceptors during the “_Select_” phase : Getting changes from the database.
Interceptors during the “_Apply_” phase : Applying Insert / Delete or Update to the database.
Interceptors for extra workloads like conflict resolution, serialization, converters & so on …
On each level you will have:
A before event: Generally ending by “_ing_” like
OnDatabaseChangesApplying
.An after event: Generally ending by “_ied_” like
OnDatabaseChangesApplied
.
Datasource level¶
OnConnectionOpen¶
The OnConnectionOpen
event is raised when a connection is opened, through the underline provider.
TODO
OnReConnect¶
The OnReConnect
event is raised when a connection is re-opened, through the underline provider.
DMS is using a custom retry policy, inspired from Polly to manage a connection retry policy.
localOrchestrator.OnReConnect(args => {
Console.WriteLine($"[Retry] Can't connect to database {args.Connection?.Database}. " +
$"Retry N°{args.Retry}. " +
$"Waiting {args.WaitingTimeSpan.Milliseconds}. Exception:{args.HandledException.Message}.");
});
You can customize the retry policy, only on http mode, when using a WebRemoteOrchestrator
instance.
var webRemoteOrchestrator = new WebRemoteOrchestrator(serviceUri);
// limit to 2 retries only
webRemoteOrchestrator.SyncPolicy.RetryCount = 2;
var webRemoteOrchestrator = new WebRemoteOrchestrator(serviceUri);
// retry for ever (not sure it's a good idea, that being said)
webRemoteOrchestrator.SyncPolicy = SyncPolicy.WaitAndRetryForever(TimeSpan.FromSeconds(1));
OnTransactionOpen¶
The OnTransactionOpen
event is raised when a transaction is opened, through the underline provider.
TODO
OnConnectionClose¶
The OnConnectionClose
event is raised when a connection is closed, through the underline provider.
TODO
OnTransactionCommit¶
The OnTransactionCommit
event is raised when a transaction is committed, through the underline provider.
TODO
OnGetCommand¶
The OnGetCommand interceptor is happening when a command is retrieved from the underline provider (SqlSyncProvider
, MySqlSyncProvider
, etc..)
agent.RemoteOrchestrator.OnGetCommand(args =>
{
if (args.Command.CommandType == CommandType.StoredProcedure)
{
args.Command.CommandText = args.Command.CommandText.Replace("_filterproducts_", "_default_");
}
});
OnExecuteCommand¶
The OnExecuteCommand
interceptor is happening when a command is about to be executed on the client or server.
agent.RemoteOrchestrator.OnExecuteCommand(args =>
{
Console.WriteLine(args.Command.CommandText);
});
Selecting changes¶
Regarding the rows selection from your client or server:
OnDatabaseChangesSelecting
: Raised before selecting rows. You have info about the tmp folder and batch size that will be used.OnTableChangesSelecting
: Raised before selecting rows for a particular table : You have info about the current table and theDbCommand
used to fetch data.
On the other side, once rows are selected, you still can:
OnRowsChangesSelected
: Raised once a row is read from the databse, but not yet serialized to disk. Row is still in memory, and connection / reader still opened.OnTableChangesSelected
: Raised once a table changes as been fully read. Changes (all batches for this table) are serialized to disk. Connection / reader are closed.OnDatabaseChangesSelected
: Raised once all changes are grabbed from the local database. Changes are serialized to disk.
OnDatabaseChangesSelecting¶
Occurs when changes are going to be queried from the underline database.
var localOrchestrator = new LocalOrchestrator(clientProvider);
localOrchestrator.OnDatabaseChangesSelecting(args => {
Console.WriteLine($"Getting changes from local database:");
Console.WriteLine($"Batch directory: {args.BatchDirectory}. Batch size: {args.BatchSize}.
Is first sync: {args.IsNew}");
Console.WriteLine($"From: {args.FromTimestamp}. To: {args.ToTimestamp}.");
}
OnTableChangesSelecting¶
Note
The Command
property can be changed here, depending on your needs.
var localOrchestrator = new LocalOrchestrator(clientProvider);
localOrchestrator.OnTableChangesSelecting(args =>
{
Console.WriteLine($"Getting changes from local database " +
$"for table:{args.SchemaTable.GetFullName()}");
Console.WriteLine($"{args.Command.CommandText}");
});
OnRowsChangesSelected¶
SyncRow
row property, the table schema and the state of the row (Modified, Deleted).SyncRow
property on the fly if needed.var localOrchestrator = new LocalOrchestrator(clientProvider);
localOrchestrator.OnRowsChangesSelected(args =>
{
Console.WriteLine($"Row read from local database for table:{args.SchemaTable.GetFullName()}");
Console.WriteLine($"{args.SyncRow}");
});
Warning
This event is raised for each row, so be careful with the number of rows you have in your database.
Plus, this event is raised during the reading phase of the database, that means that the connection is still opened.
If you have a lot of rows, you may want to use the OnTableChangesSelected
event instead, that occurs once the table is fully read, and results are serialized on disk.
OnTableChangesSelected¶
localOrchestrator.OnTableChangesSelected(args =>
{
Console.WriteLine($"Table: {args.SchemaTable.GetFullName()} read. " +
$"Rows count:{args.BatchInfo.RowsCount}.");" +
Console.WriteLine($"Directory: {args.BatchInfo.DirectoryName}. " +
$"Number of files: {args.BatchPartInfos?.Count()} ");
Console.WriteLine($"Changes: {args.TableChangesSelected.TotalChanges} " +
$"({args.TableChangesSelected.Upserts}/{args.TableChangesSelected.Deletes})");
});
Hint
You have access to the serialized rows on disk, in the BatchInfo
property.
You can iterate through all the files, and read the rows from the files, using the LoadTableFromBatchInfoAsync
OnDatabaseChangesSelected¶
BatchInfo
property is fully filled with all batch files.localOrchestrator.OnDatabaseChangesSelected(args =>
{
Console.WriteLine($"Directory: {args.BatchInfo.DirectoryName}. "
$"Number of files: {args.BatchInfo.BatchPartsInfo?.Count()} ");
Console.WriteLine($"Total: {args.ChangesSelected.TotalChangesSelected} " +
$"({args.ChangesSelected.TotalChangesSelectedUpdates}" +
$"/{args.ChangesSelected.TotalChangesSelectedDeletes})");
foreach (var table in args.ChangesSelected.TableChangesSelected)
Console.WriteLine($"Table: {table.TableName}. "
$"Total: {table.TotalChanges} ({table.Upserts / table.Deletes}");
});
Hint
You have access to the serialized rows on disk, in the BatchInfo
property.
You can iterate through all the files, and read the rows from the files, using the LoadTablesFromBatchInfoAsync
Applying changes¶
Regarding the rows to apply on your client (or server) database, you can intercept different kind of events:
OnDatabaseChangesApplying
: Rows are serialized locally in a batch info folder BUT they are not yet read internally and are not in memory. You can iterate over all the files and see if you have rows to apply.OnTableChangesApplying
: Rows are still on disk and not in memory. This interceptor is called for each table that has rows to apply.OnRowsChangesApplying
: Rows ARE now in memory, in a batch (depending on batch size and provider max batch), and are going to be applied.
On the other side, once rows are applied, you can iterate through different interceptors:
OnTableChangesApplied
: Contains a summary of all rows applied on a table for a particular state (DataRowState.Modified or Deleted).OnDatabaseChangesApplied
: Contains a summary of all changes applied on the database level.
OnDatabaseChangesApplying¶
OnDatabaseChangesApplying
interceptor is happening when changes are going to be applied on the client or server.To be able to load batches from the temporary folder, or save rows, you can use the LoadTablesFromBatchInfoAsync and SaveTableToBatchPartInfoAsync methods
localOrchestrator.OnDatabaseChangesApplying(async args =>
{
foreach (var table in args.ApplyChanges.Schema.Tables)
{
// loading in memory all batches containing rows for the current table
var syncTable = await localOrchestrator.LoadTableFromBatchInfoAsync(
args.ApplyChanges.BatchInfo, table.TableName, table.SchemaName);
Console.WriteLine($"Changes for table {table.TableName}. Rows:{syncTable.Rows.Count}");
foreach (var row in syncTable.Rows)
Console.WriteLine(row);
Console.WriteLine();
}
});
OnTableChangesApplying¶
OnTableChangesApplying
is happening right before rows are applied on the client or server.OnDatabaseChangesApplying
the changes are not yet loaded in memory. They are all stored locally in a temporary folder.// Just before applying changes locally, at the table level
localOrchestrator.OnTableChangesApplying(async args =>
{
if (args.BatchPartInfos != null)
{
var syncTable = await localOrchestrator.LoadTableFromBatchInfoAsync(
args.BatchInfo, args.SchemaTable.TableName, args.SchemaTable.SchemaName, args.State);
if (syncTable != null && syncTable.HasRows)
{
Console.WriteLine($"- --------------------------------------------");
Console.WriteLine($"- Applying [{args.State}]
changes to Table {args.SchemaTable.GetFullName()}");
foreach (var row in syncTable.Rows)
Console.WriteLine(row);
}
}
});
OnBatchChangesApplying¶
OnBatchChangesApplying
interceptor is happening when a batch for a particular table is about to be applied on the local data source.SyncOptions.BatchSize
(Default is 2 Mo)Modified
/ Deleted
).Modified
, one for Deleted
), you will fire 2000 times this interceptor.agent.LocalOrchestrator.OnBatchChangesApplying(async args =>
{
if (args.BatchPartInfo != null)
{
Console.WriteLine($"FileName:{args.BatchPartInfo.FileName}. RowsCount:{args.BatchPartInfo.RowsCount} ");
Console.WriteLine($"Applying rows from this batch part info:");
var table = await agent.LocalOrchestrator.LoadTableFromBatchPartInfoAsync(args.BatchInfo,
args.BatchPartInfo, args.State, args.Connection, args.Transaction);
foreach (var row in table.Rows)
Console.WriteLine(row);
}
});
OnRowsChangesApplying¶
The OnRowsChangesApplying
interceptor is happening just before applying a batch of rows to the local (client or server) database.
The number of rows to be applied here is depending on:
The batch size you have set in your SyncOptions instance :
SyncOptions.BatchSize
(Default is 2 Mo)The max number of rows to applied in one single instruction :
Provider.BulkBatchMaxLinesCount
(Default is 10 000 rows per instruction)
localOrchestrator.OnRowsChangesApplying(async args =>
{
Console.WriteLine($"- --------------------------------------------");
Console.WriteLine($"- In memory rows that are going to be Applied");
foreach (var row in args.SyncRows)
Console.WriteLine(row);
Console.WriteLine();
});
OnTableChangesApplied¶
The OnTableChangesApplied
interceptor is happening when all rows, for a specific table, are applied on the local (client or server) database.
TODO
OnBatchChangesApplying¶
OnBatchChangesApplied
interceptor is happening when a batch for a particular table has been applied.agent.LocalOrchestrator.OnBatchChangesApplied(async args =>
{
if (args.BatchPartInfo != null)
{
Console.WriteLine($"FileName:{args.BatchPartInfo.FileName}. RowsCount:{args.BatchPartInfo.RowsCount} ");
Console.WriteLine($"Applied rows from this batch part info:");
var table = await agent.LocalOrchestrator.LoadTableFromBatchPartInfoAsync(args.BatchInfo,
args.BatchPartInfo, args.State, args.Connection, args.Transaction);
foreach (var row in table.Rows)
Console.WriteLine(row);
}
});
OnDatabaseChangesApplied¶
The OnDatabaseChangesApplied
interceptor is happening when all changes are applied on the client or server.
TODO
Snapshots¶
See how snapshots work in the Snapshots section.
OnSnapshotCreating¶
The OnSnapshotCreating
interceptor is happening when a snapshot is going to be created from the server side
TODO
OnSnapshotCreated¶
The OnSnapshotCreated
interceptor is happening when a snapshot is created from the server side.
TODO
OnSnapshotApplying¶
The OnSnapshotApplying
interceptor is happening when a snapshot is going to be applied on the client side.
TODO
OnSnapshotApplied¶
The OnSnapshotApplied
interceptor is happening when a snapshot is applied on the client side.
TODO
Specific¶
OnProvisioning¶
The OnProvisioning
interceptor is happening when the database is being provisioned.
TODO
OnProvisioned¶
The OnProvisioned
interceptor is happening when the database is provisioned.
TODO
OnDeprovisioning¶
The OnDeprovisioning
interceptor is happening when the database is being deprovisioned.
TODO
OnDeprovisioned¶
The OnDeprovisioned
interceptor is happening when the database is deprovisioned.
TODO
OnLocalTimestampLoading¶
OnLocalTimestampLoaded¶
OnSchemaLoading¶
OnSchemaLoaded¶
OnMetadataCleaning¶
OnMetadataCleaned¶
OnApplyChangesConflictOccured¶
See Conflicts
OnApplyChangesErrorOccured¶
See Errors
OnSerializingSyncRow¶
OnDeserializingSyncRow¶
OnSessionBegin¶
OnSessionEnd¶
OnConflictingSetup¶
OnGettingOperation¶
The OnGettingOperation
interceptor is happening when a server receive a request from a client for initiate a synchronization.
From here, you have the option to override the operation, using the SyncOperation
enumeration:
public enum SyncOperation
{
/// <summary>
/// Normal synchronization
/// </summary>
Normal = 0,
/// <summary>
/// Reinitialize the whole sync database,
/// applying all rows from the server to the client
/// </summary>
Reinitialize = 1,
/// <summary>
/// Reinitialize the whole sync database,
/// applying all rows from the server to the client, after trying a client upload
/// </summary>
ReinitializeWithUpload = 2,
/// <summary>
/// Drop all the sync metadatas even tracking tables and
/// scope infos and make a full sync again
/// </summary>
DropAllAndSync = 4,
/// <summary>
/// Drop all the sync metadatas even tracking tables and
/// scope infos and exit
/// </summary>
DropAllAndExit = 8,
/// <summary>
/// Deprovision stored procedures and triggers and sync again
/// </summary>
DeprovisionAndSync = 16,
/// <summary>
/// Exit a Sync session without syncing
/// </summary>
AbortSync = 32,
}
Useful for example to force a ReinitializeWithUpload operation, when you have a conflict on the client side, and you want to force the client to upload all his changes to the server, then reinitialize everything.
Hint
This method is usefull most of the time, from the server side, when using a proxy ASP.NET Core Web API.
[HttpPost]
public async Task Post()
{
var scopeName = context.GetScopeName();
var clientScopeId = context.GetClientScopeId();
var webServerAgent = webServerAgents.First(wsa => wsa.ScopeName == scopeName);
webServerAgent.RemoteOrchestrator.OnGettingOperation(operationArgs =>
{
if (scopeName == "all" && clientScopeId == A_PARTICULAR_CLIENT_ID_TO_CHECK)
operationArgs.SyncOperation = SyncOperation.ReinitializeWithUpload;
});
await webServerAgent.HandleRequestAsync(context);
}
OnOutdated¶
The OnOutdated
interceptor is happening when a client is outdated. You can use this interceptor to force the client to reinitialize its database if it is outdated.
By default, an error is raised, and sync is stopped. This event is raised only on the client side.
agent.LocalOrchestrator.OnOutdated(oa =>
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("local database is too old to synchronize with the server.");
Console.ResetColor();
Console.WriteLine("Do you want to synchronize anyway, and potentially lost data ? ");
Console.Write("Enter a value ('r' for reinitialize or 'ru' for reinitialize with upload): ");
var answer = Console.ReadLine();
if (answer.ToLowerInvariant() == "r")
oa.Action = OutdatedAction.Reinitialize;
else if (answer.ToLowerInvariant() == "ru")
oa.Action = OutdatedAction.ReinitializeWithUpload;
});
Web¶
Some interceptors are specific to web orchestrators WebRemoteOrchestrator
& WebServerAgent
.
These orchestrators will let you intercept all the Requests
and Responses
that will be generated by DMS
during a web call.
WebServerAgent¶
The two first interceptors will intercept basically all requests and responses coming in and out:
webServerAgent.OnHttpGettingRequest(args => {})
webServerAgent.OnHttpSendingResponse(args => {})
Each of them will let you access the HttpContext, SyncContext and SessionCache instances:
webServerAgent.OnHttpGettingRequest(args =>
{
var httpContext = args.HttpContext;
var syncContext = args.Context;
var session = args.SessionCache;
});
The two last new web server http interceptors will let you intercept all the calls made when server receives client changes and when server sends back server changes.
webServerAgent.OnHttpGettingChanges(args => {});
webServerAgent.OnHttpSendingChanges(args => {});
Here is a quick example using all of them:
webServerAgent.OnHttpGettingRequest(req =>
Console.WriteLine("Receiving Client Request:" + req.Context.SyncStage +
". " + req.HttpContext.Request.Host.Host + "."));
webServerAgent.OnHttpSendingResponse(res =>
Console.WriteLine("Sending Client Response:" + res.Context.SyncStage +
". " + res.HttpContext.Request.Host.Host));
webServerAgent.OnHttpGettingChanges(args
=> Console.WriteLine("Getting Client Changes" + args));
webServerAgent.OnHttpSendingChanges(args
=> Console.WriteLine("Sending Server Changes" + args));
await webServerManager.HandleRequestAsync(context);
Receiving Client Request:ScopeLoading. localhost.
Sending Client Response:Provisioning. localhost
Receiving Client Request:ChangesSelecting. localhost.
Sending Server Changes[localhost] Sending All Snapshot Changes. Rows:0
Sending Client Response:ChangesSelecting. localhost
Receiving Client Request:ChangesSelecting. localhost.
Getting Client Changes[localhost] Getting All Changes. Rows:0
Sending Server Changes[localhost] Sending Batch Changes. (1/11). Rows:658
Sending Client Response:ChangesSelecting. localhost
Receiving Client Request:ChangesSelecting. localhost.
Sending Server Changes[localhost] Sending Batch Changes. (2/11). Rows:321
Sending Client Response:ChangesSelecting. localhost
Receiving Client Request:ChangesSelecting. localhost.
Sending Server Changes[localhost] Sending Batch Changes. (3/11). Rows:29
Sending Client Response:ChangesSelecting. localhost
Receiving Client Request:ChangesSelecting. localhost.
Sending Server Changes[localhost] Sending Batch Changes. (4/11). Rows:33
Sending Client Response:ChangesSelecting. localhost
Receiving Client Request:ChangesSelecting. localhost.
Sending Server Changes[localhost] Sending Batch Changes. (5/11). Rows:39
Sending Client Response:ChangesSelecting. localhost
Receiving Client Request:ChangesSelecting. localhost.
Sending Server Changes[localhost] Sending Batch Changes. (6/11). Rows:55
Sending Client Response:ChangesSelecting. localhost
Receiving Client Request:ChangesSelecting. localhost.
Sending Server Changes[localhost] Sending Batch Changes. (7/11). Rows:49
Sending Client Response:ChangesSelecting. localhost
Receiving Client Request:ChangesSelecting. localhost.
Sending Server Changes[localhost] Sending Batch Changes. (8/11). Rows:32
Sending Client Response:ChangesSelecting. localhost
Receiving Client Request:ChangesSelecting. localhost.
Sending Server Changes[localhost] Sending Batch Changes. (9/11). Rows:758
Sending Client Response:ChangesSelecting. localhost
Receiving Client Request:ChangesSelecting. localhost.
Sending Server Changes[localhost] Sending Batch Changes. (10/11). Rows:298
Sending Client Response:ChangesSelecting. localhost
Receiving Client Request:ChangesSelecting. localhost.
Sending Server Changes[localhost] Sending Batch Changes. (11/11). Rows:1242
Sending Client Response:ChangesSelecting. localhost
Synchronization done.
The main differences are that the two first ones will intercept ALL requests coming from the client and the two last one will intercept Only requests where data are exchanged (but you have more detailed)
WebRemoteOrchestrator¶
You have pretty much the same Http
interceptors on the client side. OnHttpGettingRequest
becomes OnHttpSendingRequest
and OnHttpSendingResponse
becomes OnHttpGettingResponse
:
localOrchestrator.OnHttpGettingResponse(req => Console.WriteLine("Receiving Server Response"));
localOrchestrator.OnHttpSendingRequest(res =>Console.WriteLine("Sending Client Request."));
localOrchestrator.OnHttpGettingChanges(args => Console.WriteLine("Getting Server Changes" + args));
localOrchestrator.OnHttpSendingChanges(args => Console.WriteLine("Sending Client Changes" + args));
Sending Client Request.
Receiving Server Response
Sending Client Request.
Receiving Server Response
Sending Client Changes[localhost] Sending All Changes. Rows:0
Sending Client Request.
Receiving Server Response
Getting Server Changes[localhost] Getting Batch Changes. (1/11). Rows:658
Sending Client Request.
Receiving Server Response
Getting Server Changes[localhost] Getting Batch Changes. (2/11). Rows:321
Sending Client Request.
Receiving Server Response
Getting Server Changes[localhost] Getting Batch Changes. (3/11). Rows:29
Sending Client Request.
Receiving Server Response
Getting Server Changes[localhost] Getting Batch Changes. (4/11). Rows:33
Sending Client Request.
Receiving Server Response
Getting Server Changes[localhost] Getting Batch Changes. (5/11). Rows:39
Sending Client Request.
Receiving Server Response
Getting Server Changes[localhost] Getting Batch Changes. (6/11). Rows:55
Sending Client Request.
Receiving Server Response
Getting Server Changes[localhost] Getting Batch Changes. (7/11). Rows:49
Sending Client Request.
Receiving Server Response
Getting Server Changes[localhost] Getting Batch Changes. (8/11). Rows:32
Sending Client Request.
Receiving Server Response
Getting Server Changes[localhost] Getting Batch Changes. (9/11). Rows:758
Sending Client Request.
Receiving Server Response
Getting Server Changes[localhost] Getting Batch Changes. (10/11). Rows:298
Sending Client Request.
Receiving Server Response
Getting Server Changes[localhost] Getting Batch Changes. (11/11). Rows:1242
Synchronization done.
Example: Hook Bearer token¶
The idea is to inject the user identifier UserId
in the SyncParameters
collection on the server, after having extract this value from a Bearer
token.
That way the UserId
is not hard coded or store somewhere on the client application, since this value is generated during the authentication part.
As you can see:
My
SyncController
is marked with the [Authorize] attribute.The orchestrator is only called when we know that the user is authenticated.
We are injecting the
UserId
value coming from the bearer into theSyncContext.Parameters
.Optionally, because we don’t want to send back this value to the client, we are removing it when sending the response.
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class SyncController : ControllerBase
{
private WebServerAgent webServerAgent;
// Injected thanks to Dependency Injection
public SyncController(WebServerAgent webServerAgent)
=> this.webServerAgent = webServerAgent;
/// <summary>
/// This POST handler is mandatory to handle all the sync process
[HttpPost]
public async Task Post()
{
// If you are using the [Authorize] attribute you don't need to check
// the User.Identity.IsAuthenticated value
if (HttpContext.User.Identity.IsAuthenticated)
{
// OPTIONAL: -------------------------------------------
// OPTIONAL: Playing with user coming from bearer token
// OPTIONAL: -------------------------------------------
// on each request coming from the client, just inject the User Id parameter
webServerAgent.OnHttpGettingRequest(args =>
{
var pUserId = args.Context.Parameters["UserId"];
if (pUserId == null)
{
var userId = this.HttpContext.User.Claims.FirstOrDefault(
x => x.Type == ClaimTypes.NameIdentifier);
args.Context.Parameters.Add("UserId", userId);
}
});
// Because we don't want to send back this value, remove it from the response
webServerAgent.OnHttpSendingResponse(args =>
{
if (args.Context.Parameters.Contains("UserId"))
args.Context.Parameters.Remove("UserId");
});
await webServerAgent.HandleRequestAsync(this.HttpContext);
}
else
{
this.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
}
}
/// <summary>
/// This GET handler is optional. It allows you to see the configuration hosted on the server
/// The configuration is shown only if Environmenent == Development
/// </summary>
[HttpGet]
[AllowAnonymous]
public Task Get() => this.HttpContext.WriteHelloAsync(webServerAgent);
}