/// <summary> /// Upserts the given <paramref name="recordSet"/>. /// </summary> public static Task UpsertZoneRecordSetAsync(this IElementEndpoint <Zone> endpoint, RecordSet recordSet, CancellationToken cancellationToken = default) { recordSet.ChangeType = ChangeType.Replace; return(endpoint.PatchRecordSetAsync(recordSet, cancellationToken)); }
/// <summary> /// Applies a JSON Patch to the entity. Sends the patch instructions to the server for processing; falls back to local processing with optimistic concurrency if that fails. /// </summary> /// <typeparam name="TEntity">The type of entity the endpoint represents.</typeparam> /// <param name="endpoint">The endpoint representing the entity.</param> /// <param name="patchAction">Callback for building a patch document describing the desired modifications.</param> /// <param name="maxRetries">The maximum number of retries to perform for optimistic concurrency before giving up.</param> /// <param name="cancellationToken">Used to cancel the request.</param> /// <returns>The <typeparamref name="TEntity"/> as returned by the server, possibly with additional fields set. <c>null</c> if the server does not respond with a result entity.</returns> /// <exception cref="NotSupportedException"><see cref="IEndpoint.Serializer"/> is not a <see cref="JsonMediaTypeFormatter"/>.</exception> /// <exception cref="InvalidDataException"><see cref="HttpStatusCode.BadRequest"/></exception> /// <exception cref="AuthenticationException"><see cref="HttpStatusCode.Unauthorized"/></exception> /// <exception cref="UnauthorizedAccessException"><see cref="HttpStatusCode.Forbidden"/></exception> /// <exception cref="KeyNotFoundException"><see cref="HttpStatusCode.NotFound"/> or <see cref="HttpStatusCode.Gone"/></exception> /// <exception cref="InvalidOperationException">The number of retries performed for optimistic concurrency exceeded <paramref name="maxRetries"/>.</exception> /// <exception cref="HttpRequestException">Other non-success status code.</exception> public static async Task <TEntity> UpdateAsync <TEntity>(this IElementEndpoint <TEntity> endpoint, Action <JsonPatchDocument <TEntity> > patchAction, int maxRetries = 3, CancellationToken cancellationToken = default) where TEntity : class { if (!(endpoint.Serializer is JsonMediaTypeFormatter serializer)) { throw new NotSupportedException($"JSON Patch can only be used if the endpoint's serializer is a {nameof(JsonMediaTypeFormatter)}."); } var patch = new JsonPatchDocument <TEntity>(new List <Operation <TEntity> >(), serializer.SerializerSettings.ContractResolver); patchAction(patch); var response = await endpoint.HttpClient.SendAsync(new HttpRequestMessage(HttpMethods.Patch, endpoint.Uri) { Content = new StringContent(JsonConvert.SerializeObject(patch)) { Headers = { ContentType = new MediaTypeHeaderValue("application/json-patch+json") } } }, cancellationToken).NoContext(); if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.MethodNotAllowed) { return(await endpoint.UpdateAsync(patch.ApplyTo, maxRetries, cancellationToken)); } if (!response.IsSuccessStatusCode) { await endpoint.ErrorHandler.HandleAsync(response).NoContext(); } return(response.Content == null ? default : await response.Content.ReadAsAsync <TEntity>(endpoint.Serializer, cancellationToken)); }
/// <summary> /// Reads the current state of the entity, applies a change to it and stores the result. Applies optimistic concurrency using automatic retries. /// </summary> /// <typeparam name="TEntity">The type of entity the endpoint represents.</typeparam> /// <param name="endpoint">The endpoint representing the entity.</param> /// <param name="updateAction">A callback that takes the current state of the entity and applies the desired modifications.</param> /// <param name="maxRetries">The maximum number of retries to perform for optimistic concurrency before giving up.</param> /// <param name="cancellationToken">Used to cancel the request.</param> /// <returns>The <typeparamref name="TEntity"/> as returned by the server, possibly with additional fields set. <c>null</c> if the server does not respond with a result entity.</returns> /// <exception cref="InvalidDataException"><see cref="HttpStatusCode.BadRequest"/></exception> /// <exception cref="AuthenticationException"><see cref="HttpStatusCode.Unauthorized"/></exception> /// <exception cref="UnauthorizedAccessException"><see cref="HttpStatusCode.Forbidden"/></exception> /// <exception cref="KeyNotFoundException"><see cref="HttpStatusCode.NotFound"/> or <see cref="HttpStatusCode.Gone"/></exception> /// <exception cref="InvalidOperationException">The number of retries performed for optimistic concurrency exceeded <paramref name="maxRetries"/>.</exception> /// <exception cref="HttpRequestException">Other non-success status code.</exception> public static async Task <TEntity> UpdateAsync <TEntity>(this IElementEndpoint <TEntity> endpoint, Action <TEntity> updateAction, int maxRetries = 3, CancellationToken cancellationToken = default) { int retryCounter = 0; while (true) { var entity = await endpoint.ReadAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); updateAction(entity); cancellationToken.ThrowIfCancellationRequested(); try { return(await endpoint.SetAsync(entity, cancellationToken)); } catch (InvalidOperationException) { if (retryCounter++ >= maxRetries) { throw; } cancellationToken.ThrowIfCancellationRequested(); } } }
private static async Task UpdateRecord( AutoSpf.FlattenedInclude flattenedInclude, Config config, Zone zone, IElementEndpoint <Zone> zoneEndpoint ) { var name = flattenedInclude.DName.Replace(config.AutoSpfDomain, config.DestinationZone); var newValue = flattenedInclude.Value.Replace(config.AutoSpfDomain, config.DestinationZone); if (!newValue.EndsWith("\"")) { newValue += "\""; } if (!newValue.StartsWith("\"")) { newValue = "\"" + newValue; } var existingRecord = zone.RecordSets.FirstOrDefault(x => x.Name.Equals(name)); if (existingRecord != null) { var needsUpdate = existingRecord.Records.Count != 1 || existingRecord.Records[0].Content != newValue; if (needsUpdate) { Console.WriteLine($"Updating {name}"); Console.WriteLine($"{newValue}"); existingRecord.ChangeType = ChangeType.Replace; existingRecord.Records.Clear(); existingRecord.Records.Add(new Record(newValue)); await zoneEndpoint.PatchRecordSetAsync(existingRecord); Console.WriteLine("Done."); Console.WriteLine(); } else { Console.WriteLine($"{name} is already up-to-date"); } } else { Console.WriteLine($"Creating {name}"); Console.WriteLine($"{newValue}"); var record = new RecordSet { ChangeType = ChangeType.Replace, Name = name, Records = new List <Record> { new(newValue) },
/// <summary> /// Gets a specific <see cref="RecordSet"/> identified by a <paramref name="recordSetName"/>. /// </summary> /// <exception cref="RecordSetNotFoundException"></exception> public static async Task <RecordSet> GetRecordSetAsync(this IElementEndpoint <Zone> endpoint, CanonicalName recordSetName, CancellationToken cancellationToken = default) { var zone = await endpoint.ReadAsync(cancellationToken); var recordSet = zone.RecordSets.FirstOrDefault(rrs => rrs.Name.Equals(recordSetName)); if (recordSet == null) { throw new RecordSetNotFoundException(recordSetName); } return(recordSet); }
/// <summary> /// Modifies existing <see cref="RecordSet"/>s. /// </summary> /// <remarks><see cref="RecordSet.ChangeType"/> must be set accordingly.</remarks> /// <exception cref="ArgumentNullException">If <see cref="RecordSet.ChangeType"/> is not set.</exception> /// <exception cref="InvalidDataException"><see cref="HttpStatusCode.BadRequest" /></exception> /// <exception cref="AuthenticationException"><see cref="HttpStatusCode.Unauthorized" /></exception> /// <exception cref="UnauthorizedAccessException"><see cref="HttpStatusCode.Forbidden" /></exception> /// <exception cref="KeyNotFoundException"><see cref="HttpStatusCode.NotFound" /> or <see cref="HttpStatusCode.Gone" /></exception> /// <exception cref="HttpRequestException">Other non-success status code.</exception> public static Task PatchRecordSetAsync(this IElementEndpoint <Zone> endpoint, RecordSet recordSet, CancellationToken cancellationToken = default) { if (!recordSet.ChangeType.HasValue) { throw new ArgumentException($"{nameof(ChangeType)} must be set.", nameof(recordSet)); } string zoneName = endpoint.Uri.AbsolutePath.Split('/').Last(); return(endpoint.MergeAsync(new Zone(zoneName) { RecordSets = { recordSet } }, cancellationToken)); }
public ZoneEndpointExtensionsFacts() { _endpoint = _endpointMock.Object; }
public override void SetUp() { base.SetUp(); _endpoint = new ElementEndpoint<MockEntity>(EntryEndpoint, "endpoint"); }
public ElementEndpointTest() { _endpoint = new ElementEndpoint <MockEntity>(EntryEndpoint, "endpoint"); }
/// <summary> /// Creates a new REST element command. /// </summary> /// <param name="endpoint">The endpoint this command operates on.</param> public ElementCommand(IElementEndpoint <TEntity> endpoint) : base(endpoint) { }