tools/code/common/Api.cs (600 lines of code) (raw):

using Azure; using Azure.Core; using Azure.Core.Pipeline; using Flurl; using LanguageExt; using Polly; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using YamlDotNet.System.Text.Json; namespace common; public sealed record ApiRevisionNumber { private uint Value { get; } private ApiRevisionNumber(uint value) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value, nameof(value)); Value = value; } public int ToInt() => (int)Value; public override string ToString() => string.Create(CultureInfo.InvariantCulture, $"{Value}"); public static ApiRevisionNumber From(int value) => new((uint)value); public static Option<ApiRevisionNumber> TryFrom(string? value) => uint.TryParse(value, out var revisionNumber) && revisionNumber > 0 ? new ApiRevisionNumber(revisionNumber) : Option<ApiRevisionNumber>.None; } public sealed record ApiName : ResourceName, IResourceName<ApiName> { private const string RevisionSeparator = ";rev="; private ApiName(string value) : base(value) { } public static ApiName From(string value) => new(value); private static Either<string, (ApiName RootName, ApiRevisionNumber RevisionNumber)> TryParseRevisionedName(string name) => name.Split(RevisionSeparator) switch { [var rootName, var revisionNumberString] => ApiRevisionNumber.TryFrom(revisionNumberString) .Map(revisionNumber => (ApiName.From(rootName), revisionNumber)) .ToEither($"'{revisionNumberString}' is not a valid revision number."), _ => $"Cannot parse name '{name}' as a revisioned API name." }; public static Either<string, (ApiName RootName, ApiRevisionNumber RevisionNumber)> TryParseRevisionedName(ApiName name) => TryParseRevisionedName(name.Value); public static bool IsNotRevisioned(ApiName name) => TryParseRevisionedName(name).IsLeft; public static bool IsRevisioned(ApiName name) => TryParseRevisionedName(name).IsRight; public static ApiName GetRootName(ApiName name) => TryParseRevisionedName(name).Map(revisionedName => revisionedName.RootName).IfLeft(name); public static ApiName GetRevisionedName(ApiName name, ApiRevisionNumber revisionNumber) { var rootName = GetRootName(name); return ApiName.From($"{rootName.Value}{RevisionSeparator}{revisionNumber.ToInt()}"); } } public sealed record ApisUri : ResourceUri { public required ManagementServiceUri ServiceUri { get; init; } private static string PathSegment { get; } = "apis"; protected override Uri Value => ServiceUri.ToUri().AppendPathSegment(PathSegment).ToUri(); public static ApisUri From(ManagementServiceUri serviceUri) => new() { ServiceUri = serviceUri }; } public sealed record ApiUri : ResourceUri { public required ApisUri Parent { get; init; } public required ApiName Name { get; init; } protected override Uri Value => Parent.ToUri().AppendPathSegment(Name.ToString()).ToUri(); public static ApiUri From(ApiName name, ManagementServiceUri serviceUri) => new() { Parent = ApisUri.From(serviceUri), Name = name }; } public sealed record ApisDirectory : ResourceDirectory { public required ManagementServiceDirectory ServiceDirectory { get; init; } private static string Name { get; } = "apis"; protected override DirectoryInfo Value => ServiceDirectory.ToDirectoryInfo().GetChildDirectory(Name); public static ApisDirectory From(ManagementServiceDirectory serviceDirectory) => new() { ServiceDirectory = serviceDirectory }; public static Option<ApisDirectory> TryParse(DirectoryInfo? directory, ManagementServiceDirectory serviceDirectory) => directory is not null && directory.Name == Name && directory.Parent?.FullName == serviceDirectory.ToDirectoryInfo().FullName ? new ApisDirectory { ServiceDirectory = serviceDirectory } : Option<ApisDirectory>.None; } public sealed record ApiDirectory : ResourceDirectory { public required ApisDirectory Parent { get; init; } public required ApiName Name { get; init; } protected override DirectoryInfo Value => Parent.ToDirectoryInfo().GetChildDirectory(Name.ToString()); public static ApiDirectory From(ApiName name, ManagementServiceDirectory serviceDirectory) => new() { Parent = ApisDirectory.From(serviceDirectory), Name = name }; public static Option<ApiDirectory> TryParse(DirectoryInfo? directory, ManagementServiceDirectory serviceDirectory) => from parent in ApisDirectory.TryParse(directory?.Parent, serviceDirectory) select new ApiDirectory { Parent = parent, Name = ApiName.From(directory!.Name) }; } public sealed record ApiInformationFile : ResourceFile { public required ApiDirectory Parent { get; init; } private static string Name { get; } = "apiInformation.json"; protected override FileInfo Value => Parent.ToDirectoryInfo().GetChildFile(Name); public static ApiInformationFile From(ApiName name, ManagementServiceDirectory serviceDirectory) => new() { Parent = new ApiDirectory { Parent = ApisDirectory.From(serviceDirectory), Name = name } }; public static Option<ApiInformationFile> TryParse(FileInfo? file, ManagementServiceDirectory serviceDirectory) => file is not null && file.Name == Name ? from parent in ApiDirectory.TryParse(file.Directory, serviceDirectory) select new ApiInformationFile { Parent = parent } : Option<ApiInformationFile>.None; } public sealed record ApiDto { [JsonPropertyName("properties")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public required ApiCreateOrUpdateProperties Properties { get; init; } public record ApiCreateOrUpdateProperties { [JsonPropertyName("path")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Path { get; init; } [JsonPropertyName("apiRevision")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? ApiRevision { get; init; } [JsonPropertyName("apiRevisionDescription")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? ApiRevisionDescription { get; init; } [JsonPropertyName("apiVersion")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? ApiVersion { get; init; } [JsonPropertyName("apiVersionDescription")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? ApiVersionDescription { get; init; } [JsonPropertyName("apiVersionSetId")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? ApiVersionSetId { get; init; } [JsonPropertyName("authenticationSettings")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public AuthenticationSettingsContract? AuthenticationSettings { get; init; } [JsonPropertyName("contact")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ApiContactInformation? Contact { get; init; } [JsonPropertyName("description")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Description { get; init; } [JsonPropertyName("isCurrent")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool? IsCurrent { get; init; } [JsonPropertyName("license")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ApiLicenseInformation? License { get; init; } [JsonPropertyName("apiType")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? ApiType { get; init; } [JsonPropertyName("apiVersionSet")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ApiVersionSetContractDetails? ApiVersionSet { get; init; } [JsonPropertyName("displayName")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? DisplayName { get; init; } [JsonPropertyName("format")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Format { get; init; } [JsonPropertyName("protocols")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ImmutableArray<string>? Protocols { get; init; } [JsonPropertyName("serviceUrl")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] #pragma warning disable CA1056 // URI-like properties should not be strings public string? ServiceUrl { get; init; } #pragma warning restore CA1056 // URI-like properties should not be strings [JsonPropertyName("sourceApiId")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? SourceApiId { get; init; } [JsonPropertyName("translateRequiredQueryParameters")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? TranslateRequiredQueryParameters { get; init; } [JsonPropertyName("value")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Value { get; init; } [JsonPropertyName("wsdlSelector")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public WsdlSelectorContract? WsdlSelector { get; init; } [JsonPropertyName("subscriptionKeyParameterNames")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public SubscriptionKeyParameterNamesContract? SubscriptionKeyParameterNames { get; init; } [JsonPropertyName("subscriptionRequired")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool? SubscriptionRequired { get; init; } [JsonPropertyName("termsOfServiceUrl")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] #pragma warning disable CA1056 // URI-like properties should not be strings public string? TermsOfServiceUrl { get; init; } #pragma warning restore CA1056 // URI-like properties should not be strings [JsonPropertyName("type")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Type { get; init; } public record AuthenticationSettingsContract { [JsonPropertyName("oAuth2")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public OAuth2AuthenticationSettingsContract? OAuth2 { get; init; } [JsonPropertyName("openid")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public OpenIdAuthenticationSettingsContract? OpenId { get; init; } } public record OAuth2AuthenticationSettingsContract { [JsonPropertyName("authorizationServerId")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? AuthorizationServerId { get; init; } [JsonPropertyName("scope")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Scope { get; init; } } public record OpenIdAuthenticationSettingsContract { [JsonPropertyName("bearerTokenSendingMethods")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ImmutableArray<string>? BearerTokenSendingMethods { get; init; } [JsonPropertyName("openidProviderId")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? OpenIdProviderId { get; init; } } public record ApiContactInformation { [JsonPropertyName("email")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Email { get; init; } [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Name { get; init; } [JsonPropertyName("url")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] #pragma warning disable CA1056 // URI-like properties should not be strings public string? Url { get; init; } #pragma warning restore CA1056 // URI-like properties should not be strings } public record ApiLicenseInformation { [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Name { get; init; } [JsonPropertyName("url")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] #pragma warning disable CA1056 // URI-like properties should not be strings public string? Url { get; init; } #pragma warning restore CA1056 // URI-like properties should not be strings } public record ApiVersionSetContractDetails { [JsonPropertyName("description")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Description { get; init; } [JsonPropertyName("id")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Id { get; init; } [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Name { get; init; } [JsonPropertyName("versionHeaderName")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? VersionHeaderName { get; init; } [JsonPropertyName("versionQueryName")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? VersionQueryName { get; init; } [JsonPropertyName("versioningScheme")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? VersioningScheme { get; init; } } public record SubscriptionKeyParameterNamesContract { [JsonPropertyName("header")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Header { get; init; } [JsonPropertyName("query")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Query { get; init; } } public record WsdlSelectorContract { [JsonPropertyName("wsdlEndpointName")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? WsdlEndpointName { get; init; } [JsonPropertyName("wsdlServiceName")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? WsdlServiceName { get; init; } } } } public static class ApiModule { public static async ValueTask DeleteAll(this ApisUri uri, HttpPipeline pipeline, CancellationToken cancellationToken) => await uri.ListNames(pipeline, cancellationToken) .GroupBy(ApiName.GetRootName) .IterParallel(async group => await ApiUri.From(group.Key, uri.ServiceUri) .DeleteAllRevisions(pipeline, cancellationToken), cancellationToken); public static IAsyncEnumerable<ApiName> ListNames(this ApisUri uri, HttpPipeline pipeline, CancellationToken cancellationToken) => pipeline.ListJsonObjects(uri.ToUri(), cancellationToken) .Select(jsonObject => jsonObject.GetStringProperty("name")) .Select(ApiName.From); public static IAsyncEnumerable<(ApiName Name, ApiDto Dto)> List(this ApisUri apisUri, HttpPipeline pipeline, CancellationToken cancellationToken) => apisUri.ListNames(pipeline, cancellationToken) .SelectAwait(async name => { var uri = new ApiUri { Parent = apisUri, Name = name }; var dto = await uri.GetDto(pipeline, cancellationToken); return (name, dto); }); public static async ValueTask<Option<ApiDto>> TryGetDto(this ApiUri uri, HttpPipeline pipeline, CancellationToken cancellationToken) { var contentOption = await pipeline.GetContentOption(uri.ToUri(), cancellationToken); return contentOption.Map(content => content.ToObjectFromJson<ApiDto>()); } public static async ValueTask<ApiDto> GetDto(this ApiUri uri, HttpPipeline pipeline, CancellationToken cancellationToken) { var content = await pipeline.GetContent(uri.ToUri(), cancellationToken); return content.ToObjectFromJson<ApiDto>(); } public static async ValueTask<Option<BinaryData>> TryGetSpecificationContents(this ApiUri apiUri, ApiSpecification specification, HttpPipeline pipeline, CancellationToken cancellationToken) { if (specification is ApiSpecification.GraphQl) { return await apiUri.TryGetGraphQlSchema(pipeline, cancellationToken); } BinaryData? content; try { var exportUri = GetExportUri(apiUri, specification, includeLink: true); var downloadUri = await GetSpecificationDownloadUri(exportUri, pipeline, cancellationToken); var nonAuthenticatedHttpPipeline = HttpPipelineBuilder.Build(ClientOptions.Default); content = await nonAuthenticatedHttpPipeline.GetContent(downloadUri, cancellationToken); } // If we can't download the specification through the download link, get it directly. catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.InternalServerError) { // Don't export XML specifications, as the non-link exports cannot be reimported. if (specification is ApiSpecification.Wsdl or ApiSpecification.Wadl) { return Option<BinaryData>.None; } var exportUri = GetExportUri(apiUri, specification, includeLink: false); var json = await pipeline.GetJsonObject(exportUri, cancellationToken); var contentString = json.GetProperty("value") switch { JsonValue jsonValue => jsonValue.ToString(), var node => node.ToJsonString(JsonObjectExtensions.SerializerOptions) }; content = BinaryData.FromString(contentString); } // APIM exports OpenApiV2 to JSON. Convert to YAML if needed. if (specification is ApiSpecification.OpenApi openApi && openApi.Format is OpenApiFormat.Yaml && openApi.Version is OpenApiVersion.V2) { var yaml = YamlConverter.SerializeJson(content.ToString()); content = BinaryData.FromString(yaml); } return content; } private static Uri GetExportUri(ApiUri apiUri, ApiSpecification specification, bool includeLink) { var format = GetExportFormat(specification, includeLink); return apiUri.ToUri() .SetQueryParam("format", format) .SetQueryParam("export", "true") .SetQueryParam("api-version", "2022-09-01-preview") .ToUri(); } private static string GetExportFormat(ApiSpecification specification, bool includeLink) { var formatWithoutLink = specification switch { ApiSpecification.Wadl => "wadl", ApiSpecification.Wsdl => "wsdl", ApiSpecification.OpenApi openApiSpecification => (openApiSpecification.Version, openApiSpecification.Format) switch { (OpenApiVersion.V2, _) => "swagger", (OpenApiVersion.V3, OpenApiFormat.Yaml) => "openapi", (OpenApiVersion.V3, OpenApiFormat.Json) => "openapi+json", _ => throw new NotSupportedException() }, _ => throw new NotSupportedException() }; return includeLink ? $"{formatWithoutLink}-link" : formatWithoutLink; } private static async ValueTask<Uri> GetSpecificationDownloadUri(Uri exportUri, HttpPipeline pipeline, CancellationToken cancellationToken) { var json = await pipeline.GetJsonObject(exportUri, cancellationToken); return json.GetJsonObjectProperty("value") .GetAbsoluteUriProperty("link"); } public static async ValueTask Delete(this ApiUri uri, HttpPipeline pipeline, CancellationToken cancellationToken) => await pipeline.DeleteResource(uri.ToUri(), waitForCompletion: true, cancellationToken); public static async ValueTask DeleteAllRevisions(this ApiUri uri, HttpPipeline pipeline, CancellationToken cancellationToken) => await pipeline.DeleteResource(uri.ToUri() .SetQueryParam("deleteRevisions", "true") .ToUri(), waitForCompletion: true, cancellationToken); public static async ValueTask PutDto(this ApiUri uri, ApiDto dto, HttpPipeline pipeline, CancellationToken cancellationToken) { if (dto.Properties.Format is null && dto.Properties.Value is null) { var content = BinaryData.FromObjectAsJson(dto); await pipeline.PutContent(uri.ToUri(), content, cancellationToken); } else { if (dto.Properties.Type is "soap") { await PutSoapApi(uri, dto, pipeline, cancellationToken); } else { await PutNonSoapApi(uri, dto, pipeline, cancellationToken); } } using var _ = await new ResiliencePipelineBuilder<Response>() .AddRetry(new() { BackoffType = DelayBackoffType.Exponential, UseJitter = true, MaxRetryAttempts = 5, ShouldHandle = new PredicateBuilder<Response>().HandleResult(CreationInProgress) }) .Build() .ExecuteAsync(async cancellationToken => { using var request = pipeline.CreateRequest(uri.ToUri(), RequestMethod.Get); return await pipeline.SendRequestAsync(request, cancellationToken); }, cancellationToken); } private static async ValueTask PutSoapApi(ApiUri uri, ApiDto dto, HttpPipeline pipeline, CancellationToken cancellationToken) { // Import API with specification var soapUri = uri.ToUri().SetQueryParam("import", "true").ToUri(); var soapDto = new ApiDto { Properties = new ApiDto.ApiCreateOrUpdateProperties { Format = "wsdl", Value = dto.Properties.Value, ApiType = "soap", DisplayName = dto.Properties.DisplayName, Path = dto.Properties.Path, Protocols = dto.Properties.Protocols, ApiVersion = dto.Properties.ApiVersion, ApiVersionDescription = dto.Properties.ApiVersionDescription, ApiVersionSetId = dto.Properties.ApiVersionSetId } }; await pipeline.PutContent(soapUri, BinaryData.FromObjectAsJson(soapDto), cancellationToken); // Put API again without specification var updatedDto = dto with { Properties = dto.Properties with { Format = null, Value = null } }; // SOAP apis sometimes fail on put; retry if needed await soapApiResiliencePipeline.Value .ExecuteAsync(async cancellationToken => await pipeline.PutContent(uri.ToUri(), BinaryData.FromObjectAsJson(updatedDto), cancellationToken), cancellationToken); } private static readonly Lazy<ResiliencePipeline> soapApiResiliencePipeline = new(() => new ResiliencePipelineBuilder() .AddRetry(new() { BackoffType = DelayBackoffType.Exponential, UseJitter = true, MaxRetryAttempts = 3, ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>(exception => exception.StatusCode == HttpStatusCode.Conflict && exception.Message.Contains("IdentifierAlreadyInUse", StringComparison.OrdinalIgnoreCase)) }) .Build()); private static async ValueTask PutNonSoapApi(ApiUri uri, ApiDto dto, HttpPipeline pipeline, CancellationToken cancellationToken) { // Put API without the specification. var modelWithoutSpecification = dto with { Properties = dto.Properties with { Format = null, Value = null, ServiceUrl = null, Type = dto.Properties.Type, ApiType = dto.Properties.ApiType } }; await pipeline.PutContent(uri.ToUri(), BinaryData.FromObjectAsJson(modelWithoutSpecification), cancellationToken); // Put API again with specification await pipeline.PutContent(uri.ToUri(), BinaryData.FromObjectAsJson(dto), cancellationToken); } private static bool CreationInProgress(Response response) { if (response.Status != (int)HttpStatusCode.Created) { return false; } if (response.Headers.Any(header => header.Name.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) && header.Value.Contains("application/json", StringComparison.OrdinalIgnoreCase)) is false) { return false; } try { return response.Content.ToObjectFromJson<JsonObject>() .TryGetJsonObjectProperty("properties") .Bind(json => json.TryGetStringProperty("ProvisioningState")) .ToOption() .Where(state => state.Equals("InProgress", StringComparison.OrdinalIgnoreCase)) .IsSome; } catch (JsonException) { return false; } } public static async ValueTask PutGraphQlSchema(this ApiUri uri, BinaryData schema, HttpPipeline pipeline, CancellationToken cancellationToken) { var contents = BinaryData.FromObjectAsJson(new JsonObject() { ["properties"] = new JsonObject { ["contentType"] = "application/vnd.ms-azure-apim.graphql.schema", ["document"] = new JsonObject() { ["value"] = schema.ToString() } } }); await pipeline.PutContent(uri.ToUri() .AppendPathSegment("schemas") .AppendPathSegment("graphql") .ToUri(), contents, cancellationToken); } public static async ValueTask<Option<BinaryData>> TryGetGraphQlSchema(this ApiUri uri, HttpPipeline pipeline, CancellationToken cancellationToken) { var schemaUri = uri.ToUri() .AppendPathSegment("schemas") .AppendPathSegment("graphql") .ToUri(); var schemaJsonOption = await pipeline.GetJsonObjectOption(schemaUri, cancellationToken); return schemaJsonOption.Map(GetGraphQlSpecificationFromSchemaResponse); } private static BinaryData GetGraphQlSpecificationFromSchemaResponse(JsonObject responseJson) { var schema = responseJson.GetJsonObjectProperty("properties") .GetJsonObjectProperty("document") .GetNonEmptyOrWhiteSpaceStringProperty("value"); return BinaryData.FromString(schema); } public static IEnumerable<ApiDirectory> ListDirectories(ManagementServiceDirectory serviceDirectory) { var apisDirectory = ApisDirectory.From(serviceDirectory); return apisDirectory.ToDirectoryInfo() .ListDirectories("*") .Select(directoryInfo => ApiName.From(directoryInfo.Name)) .Select(name => new ApiDirectory { Parent = apisDirectory, Name = name }); } public static IEnumerable<ApiInformationFile> ListInformationFiles(ManagementServiceDirectory serviceDirectory) => ListDirectories(serviceDirectory) .Select(directory => new ApiInformationFile { Parent = directory }) .Where(informationFile => informationFile.ToFileInfo().Exists()); public static IAsyncEnumerable<ApiSpecificationFile> ListSpecificationFiles(ManagementServiceDirectory serviceDirectory) => ListDirectories(serviceDirectory) .SelectMany(directory => directory.ToDirectoryInfo().ListFiles("*")) .ToAsyncEnumerable() .Choose(async (file, cancellationToken) => await ApiSpecificationFile.TryParse(file, serviceDirectory, cancellationToken)); public static async ValueTask WriteDto(this ApiInformationFile file, ApiDto dto, CancellationToken cancellationToken) { var content = BinaryData.FromObjectAsJson(dto, JsonObjectExtensions.SerializerOptions); await file.ToFileInfo().OverwriteWithBinaryData(content, cancellationToken); } public static async ValueTask<ApiDto> ReadDto(this ApiInformationFile file, CancellationToken cancellationToken) { var content = await file.ToFileInfo().ReadAsBinaryData(cancellationToken); return content.ToObjectFromJson<ApiDto>(); } public static async ValueTask WriteSpecification(this ApiSpecificationFile file, BinaryData contents, CancellationToken cancellationToken) => await file.ToFileInfo().OverwriteWithBinaryData(contents, cancellationToken); public static Option<VersionSetName> TryGetVersionSetName(ApiDto dto) => from versionSetId in Prelude.Optional(dto.Properties.ApiVersionSetId) from versionSetNameString in versionSetId.Split('/') .LastOrNone() select VersionSetName.From(versionSetNameString); }