@typespec/http-server-csharp
Version:
TypeSpec service code generator for c-sharp
929 lines (922 loc) • 61.7 kB
JavaScript
import { CodeTypeEmitter, StringBuilder, code, createAssetEmitter, } from "@typespec/asset-emitter";
import { getDoc, getNamespaceFullName, getService, isErrorModel, isNeverType, isNullType, isNumericType, isTemplateDeclaration, isVoidType, resolvePath, serializeValueAsJson, } from "@typespec/compiler";
import { createRekeyableMap } from "@typespec/compiler/utils";
import { getHeaderFieldName, getHttpOperation, getHttpPart, isHeader, isStatusCode, } from "@typespec/http";
import { getUniqueItems } from "@typespec/json-schema";
import { getResourceOperation } from "@typespec/rest";
import { execFile } from "child_process";
import path from "path";
import { getEncodedNameAttribute } from "./attributes.js";
import { GeneratedFileHeader, GeneratedFileHeaderWithNullable, getSerializationSourceFiles, } from "./boilerplate.js";
import { getProjectDocs } from "./doc.js";
import { CSharpSourceType, CSharpType, CollectionType, NameCasingType, checkOrAddNamespaceToScope, } from "./interfaces.js";
import { CSharpServiceOptions, reportDiagnostic } from "./lib.js";
import { getProjectHelpers } from "./project.js";
import { getBusinessLogicImplementations, getScaffoldingHelpers, } from "./scaffolding.js";
import { getEnumType, getRecordType, isKnownReferenceType } from "./type-helpers.js";
import { CSharpOperationHelpers, HttpMetadata, ModelInfo, UnknownType, coalesceTypes, coalesceUnionTypes, ensureCSharpIdentifier, ensureCleanDirectory, formatComment, getBusinessLogicCallParameters, getBusinessLogicDeclParameters, getCSharpIdentifier, getCSharpStatusCode, getCSharpType, getCSharpTypeForIntrinsic, getCSharpTypeForScalar, getFreePort, getHttpDeclParameters, getImports, getModelAttributes, getModelDeclarationName, getModelInstantiationName, getOpenApiConfig, getOperationVerbDecorator, getStatusCode, isEmptyResponseModel, isRecord, isStringEnumType, isValueType, resolveReferenceFromScopes, } from "./utils.js";
export async function $onEmit(context) {
let _unionCounter = 0;
const controllers = new Map();
const NoResourceContext = "RPCOperations";
const doNotEmit = context.program.compilerOptions.dryRun || false;
CSharpServiceOptions.getInstance().initialize(context.options);
function getFileWriter(program) {
return async (path) => !!(await program.host.stat(resolvePath(path)).catch((_) => false));
}
class CSharpCodeEmitter extends CodeTypeEmitter {
#metadateMap = new Map();
#generatedFileHeaderWithNullable = GeneratedFileHeaderWithNullable;
#generatedFileHeader = GeneratedFileHeader;
#sourceTypeKey = "sourceType";
#nsKey = "ResolvedNamespace";
#namespaces = new Map();
#emitterOutputType = context.options["output-type"];
#emitMocks = context.options["emit-mocks"];
#useSwagger = context.options["use-swaggerui"] || false;
#openapiPath = context.options["openapi-path"] || "openapi/openapi.yaml";
#mockRegistrations = new Map();
#opHelpers = new CSharpOperationHelpers(this.emitter);
#fileExists = getFileWriter(this.emitter.getProgram());
#getOrAddNamespaceForType(type) {
const program = this.emitter.getProgram();
switch (type.kind) {
case "Boolean":
case "String":
case "StringTemplate":
case "Number":
case "Scalar":
case "Intrinsic":
case "FunctionParameter":
case "ScalarConstructor":
case "StringTemplateSpan":
case "TemplateParameter":
case "Tuple":
return undefined;
case "EnumMember":
return this.#getOrAddNamespaceForType(type.enum);
case "ModelProperty":
case "UnionVariant":
return this.#getOrAddNamespaceForType(type.type);
case "Model":
if (isRecord(type) && type.indexer)
return this.#getOrAddNamespaceForType(type.indexer.value);
if (type.indexer !== undefined && isNumericType(program, type.indexer?.key))
return this.#getOrAddNamespaceForType(type.indexer.value);
return this.#getOrAddNamespace(type.namespace);
case "Union":
if (isStringEnumType(program, type))
return this.#getOrAddNamespace(type.namespace);
const unionType = this.#coalesceTsUnion(type);
if (unionType === undefined)
return undefined;
return this.#getOrAddNamespaceForType(unionType);
default:
return this.#getOrAddNamespace(type.namespace);
}
}
#coalesceTsUnion(union) {
let result = undefined;
for (const [_, variant] of union.variants) {
if (!isNullType(variant.type)) {
if (result !== undefined && result !== variant.type)
return undefined;
result = variant.type;
}
}
return result;
}
#getOrAddNamespace(typespecNamespace) {
if (!typespecNamespace?.name)
return this.#getDefaultNamespace();
let resolvedNamespace = this.#namespaces.get(typespecNamespace);
if (resolvedNamespace !== undefined)
return resolvedNamespace;
const nsName = getNamespaceFullName(typespecNamespace);
resolvedNamespace = ensureCSharpIdentifier(this.emitter.getProgram(), typespecNamespace, nsName, NameCasingType.Namespace);
this.#namespaces.set(typespecNamespace, resolvedNamespace);
return resolvedNamespace;
}
#getDefaultNamespace() {
return "TypeSpec.Service";
}
arrayDeclaration(array, name, elementType) {
return this.collectionDeclaration(elementType, array);
}
arrayLiteral(array, elementType) {
this.emitter.emitType(elementType, this.emitter.getContext());
const csType = getCSharpType(this.emitter.getProgram(), array, this.#getOrAddNamespaceForType(elementType));
if (csType?.type) {
return code `${csType.type.getTypeReference(this.emitter.getContext()?.scope)}`;
}
return this.collectionDeclaration(elementType, array);
}
collectionDeclaration(elementType, array) {
const isUniqueItems = getUniqueItems(this.emitter.getProgram(), array);
const collectionType = isUniqueItems
? CollectionType.ISet
: CSharpServiceOptions.getInstance().collectionType;
switch (collectionType) {
case CollectionType.ISet:
return code `ISet<${this.emitter.emitTypeReference(elementType, this.emitter.getContext())}>`;
case CollectionType.IEnumerable:
return code `IEnumerable<${this.emitter.emitTypeReference(elementType, this.emitter.getContext())}>`;
case CollectionType.Array:
default:
return code `${this.emitter.emitTypeReference(elementType, this.emitter.getContext())}[]`;
}
}
booleanLiteral(boolean) {
return code `${boolean.value === true ? "true" : "false"}`;
}
unionLiteral(union) {
const csType = coalesceUnionTypes(this.emitter.getProgram(), union);
return this.emitter.result.rawCode(csType ? csType.getTypeReference(this.emitter.getContext()?.scope) : "object");
}
declarationName(declarationType) {
switch (declarationType.kind) {
case "Enum":
if (!declarationType.name)
return `Enum${_unionCounter++}`;
return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
case "Interface":
if (!declarationType.name)
return `Interface${_unionCounter++}`;
return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
case "Model":
if (!declarationType.name)
return getModelDeclarationName(this.emitter.getProgram(), declarationType, `${_unionCounter++}`);
return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
case "Operation":
return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
case "Union":
if (!declarationType.name)
return `Union${_unionCounter++}`;
return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
case "Scalar":
default:
return getCSharpIdentifier(declarationType.name, NameCasingType.Variable);
}
}
enumDeclaration(en, name) {
if (getEnumType(en) === "double")
return "";
const program = this.emitter.getProgram();
const enumName = ensureCSharpIdentifier(program, en, name);
const namespace = this.emitter.getContext().namespace;
const doc = getDoc(this.emitter.getProgram(), en);
const attributes = getModelAttributes(program, en, enumName);
this.#metadateMap.set(en, new CSharpType({ name: enumName, namespace: namespace }));
return this.emitter.result.declaration(enumName, code `
namespace ${namespace}
{
${doc ? `${formatComment(doc)}` : ""}
${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}
public enum ${enumName}
{
${this.emitter.emitEnumMembers(en)}
}
} `);
}
enumDeclarationContext(en) {
if (getEnumType(en) === "double")
return this.emitter.getContext();
const enumName = ensureCSharpIdentifier(this.emitter.getProgram(), en, en.name);
const enumFile = this.emitter.createSourceFile(`generated/models/${enumName}.cs`);
enumFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model;
const enumNamespace = this.#getOrAddNamespace(en.namespace);
return this.#createEnumContext(enumNamespace, enumFile, enumName);
}
enumMembers(en) {
const enumType = getEnumType(en);
const result = new StringBuilder();
let i = 0;
for (const [name, member] of en.members) {
i++;
const memberName = ensureCSharpIdentifier(this.emitter.getProgram(), member, name);
this.#metadateMap.set(member, { name: memberName });
if (enumType === "string") {
result.push(code `
[JsonStringEnumMemberName("${member.value ? member.value : name}")]
${ensureCSharpIdentifier(this.emitter.getProgram(), member, name)}`);
if (i < en.members.size)
result.pushLiteralSegment(",\n");
}
else if (member.value !== undefined) {
result.push(code `
${ensureCSharpIdentifier(this.emitter.getProgram(), member, name)} = ${member.value.toString()}`);
if (i < en.members.size)
result.pushLiteralSegment(",\n");
}
}
return this.emitter.result.rawCode(result.reduce());
}
intrinsic(intrinsic, name) {
switch (intrinsic.name) {
case "unknown":
case "null":
return this.emitter.result.rawCode(code `${UnknownType.getTypeReference(this.emitter.getContext().scope)}`);
case "ErrorType":
case "never":
reportDiagnostic(this.emitter.getProgram(), {
code: "invalid-intrinsic",
target: intrinsic,
format: { typeName: intrinsic.name },
});
return "";
case "void":
const type = getCSharpTypeForIntrinsic(this.emitter.getProgram(), intrinsic);
return this.emitter.result.rawCode(`${type?.type.getTypeReference(this.emitter.getContext().scope)}`);
}
}
#emitUsings(file) {
const builder = new StringBuilder();
if (file === undefined) {
file = this.emitter.getContext().file;
}
for (const ns of [...file.imports.keys()])
builder.pushLiteralSegment(`using ${ns};`);
return builder.segments.join("\n");
}
modelDeclaration(model, name) {
const parts = this.#getMultipartParts(model);
if (parts.length > 0) {
parts.forEach((p) => this.emitter.emitType(p));
return "";
}
const isErrorType = isErrorModel(this.emitter.getProgram(), model);
if (model.baseModel && model.baseModel.namespace !== model.namespace) {
const resolvedNs = this.#getOrAddNamespaceForType(model.baseModel);
if (resolvedNs)
checkOrAddNamespaceToScope(resolvedNs, this.emitter.getContext().scope);
}
const baseModelRef = model.baseModel
? code `: ${this.emitter.emitTypeReference(model.baseModel, this.emitter.getContext())}`
: "";
const baseClass = baseModelRef || (isErrorType ? ": HttpServiceException" : "");
const namespace = this.emitter.getContext().namespace;
const className = ensureCSharpIdentifier(this.emitter.getProgram(), model, name);
const doc = getDoc(this.emitter.getProgram(), model);
const attributes = getModelAttributes(this.emitter.getProgram(), model, className);
const exceptionConstructor = isErrorType
? this.getModelExceptionConstructor(this.emitter.getProgram(), model, name, className)
: "";
this.#metadateMap.set(model, new CSharpType({ name: className, namespace: namespace }));
const decl = this.emitter.result.declaration(className, code `
namespace ${namespace} {
${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public partial class ${className} ${baseClass} {
${exceptionConstructor ? `${exceptionConstructor}\n` : ""}${this.emitter.emitModelProperties(model)}
}
} `);
return decl;
}
getModelExceptionConstructor(program, model, modelName, className) {
if (!isErrorModel(program, model))
return undefined;
const constructor = this.getExceptionConstructorData(program, model, modelName);
const isParent = !!model.derivedModels?.length;
return `public ${className}(${constructor.properties}) : base(${constructor.statusCode?.value ?? `400`}${constructor.header ? `, \n\t\t headers: new(){${constructor.header}}` : ""}${constructor.value ? `, \n\t\t value: new{${constructor.value}}` : ""})
{ ${constructor.body ? `\n${constructor.body}` : ""}\n\t}${isParent ? `\npublic ${className}(int statusCode, object? value = null, Dictionary<string, string>? headers = default): base(statusCode, value, headers) {}\n` : ""}`;
}
isDuplicateExceptionName(name) {
const exceptionPropertyNames = [
"value",
"headers",
"stacktrace",
"source",
"message",
"innerexception",
"hresult",
"data",
"targetsite",
"helplink",
];
return exceptionPropertyNames.includes(name.toLowerCase());
}
getExceptionConstructorData(program, model, modelName) {
const allProperties = new ModelInfo().getAllProperties(program, model) ?? [];
const propertiesWithDefaults = allProperties.map((prop) => {
const { defaultValue: typeDefault } = this.#findPropertyType(prop);
const defaultValue = prop.defaultValue
? code `${JSON.stringify(serializeValueAsJson(program, prop.defaultValue, prop))}`
: typeDefault;
return { prop, defaultValue };
});
const sortedProperties = propertiesWithDefaults
.filter(({ prop }) => !isStatusCode(program, prop))
.sort(({ prop: a, defaultValue: aDefault }, { prop: b, defaultValue: bDefault }) => {
if (!a.optional && !aDefault && (b.optional || bDefault))
return -1;
if (!b.optional && !bDefault && (a.optional || aDefault))
return 1;
return 0;
});
const properties = [];
const body = [];
const header = [];
const value = [];
const statusCode = getStatusCode(program, model);
if (statusCode?.requiresConstructorArgument) {
properties.push(`int ${statusCode.value}`);
}
for (const { prop, defaultValue } of sortedProperties) {
let propertyName = ensureCSharpIdentifier(program, prop, prop.name);
if (modelName === propertyName || this.isDuplicateExceptionName(propertyName)) {
propertyName = `${propertyName}Prop`;
}
const type = getCSharpType(program, prop.type);
properties.push(`${type?.type.name} ${prop.name}${defaultValue ? ` = ${defaultValue}` : `${prop.optional ? " = default" : ""}`}`);
body.push(`\t\t${propertyName} = ${prop.name};`);
if (isHeader(program, prop)) {
const headerName = getHeaderFieldName(program, prop);
header.push(`{"${headerName}", ${prop.name}}`);
}
else {
value.push(`${prop.name} = ${prop.name}`);
}
}
return {
properties: properties.join(", "),
body: body.join("\n"),
header: header.join(", "),
value: value.join(","),
statusCode: statusCode,
};
}
modelDeclarationContext(model, name) {
function getSourceModels(source, visited = new Set()) {
const result = [];
if (visited.has(source))
return [];
result.push(source);
visited.add(source);
for (const candidate of source.sourceModels) {
result.push(...getSourceModels(candidate.model, visited));
}
return result;
}
function getModelNamespace(model) {
const sourceModels = getSourceModels(model);
if (sourceModels.length === 0)
return undefined;
return sourceModels
.filter((m) => m.namespace !== undefined)
.flatMap((s) => s.namespace)
.reduce((c, n) => (c === n ? c : undefined));
}
if (this.#isMultipartModel(model))
return this.emitter.getContext();
const modelName = ensureCSharpIdentifier(this.emitter.getProgram(), model, name);
const modelFile = this.emitter.createSourceFile(`generated/models/${modelName}.cs`);
modelFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model;
const ns = model.namespace ?? getModelNamespace(model);
const modelNamespace = this.#getOrAddNamespace(ns);
return this.#createModelContext(modelNamespace, modelFile, modelName);
}
modelInstantiationContext(model) {
if (this.#isMultipartModel(model))
return this.emitter.getContext();
const modelName = getModelInstantiationName(this.emitter.getProgram(), model, model.name);
const sourceFile = this.emitter.createSourceFile(`generated/models/${modelName}.cs`);
sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model;
const modelNamespace = this.#getOrAddNamespace(model.namespace);
const context = this.#createModelContext(modelNamespace, sourceFile, model.name);
context.instantiationName = modelName;
return context;
}
modelInstantiation(model, name) {
const parts = this.#getMultipartParts(model);
if (parts.length > 0) {
parts.forEach((p) => this.emitter.emitType(p));
return "";
}
const program = this.emitter.getProgram();
const recordType = getRecordType(program, model);
if (recordType !== undefined) {
const valueType = this.#getSimpleType(recordType);
return code `Dictionary<string, ${valueType ? valueType?.getTypeReference() : this.emitter.emitTypeReference(recordType)}>`;
}
const context = this.emitter.getContext();
const className = context.instantiationName ?? name;
return this.modelDeclaration(model, className);
}
#getSimpleType(type) {
switch (type.kind) {
case "Model":
if (!isRecord(type))
return undefined;
return getCSharpType(this.emitter.getProgram(), type)?.type;
case "Boolean":
case "Intrinsic":
case "ModelProperty":
case "Number":
case "Scalar":
case "String":
case "StringTemplate":
case "StringTemplateSpan":
case "Tuple":
return getCSharpType(this.emitter.getProgram(), type)?.type;
default:
return undefined;
}
}
#getMultipartParts(model) {
const parts = [...model.properties.values()]
.flatMap((p) => getHttpPart(this.emitter.getProgram(), p.type)?.type)
.filter((t) => t !== undefined);
if (model.baseModel) {
return parts.concat(this.#getMultipartParts(model.baseModel));
}
return parts;
}
#isMultipartModel(model) {
const multipartTypes = this.#getMultipartParts(model);
return multipartTypes.length > 0;
}
modelProperties(model) {
const result = new StringBuilder();
for (const [_, prop] of model.properties) {
if (!isVoidType(prop.type) &&
!isNeverType(prop.type) &&
!isNullType(prop.type) &&
!isErrorModel(this.emitter.getProgram(), prop.type)) {
result.push(code `${this.emitter.emitModelProperty(prop)}`);
}
}
return result.reduce();
}
modelLiteralContext(model) {
const name = this.emitter.emitDeclarationName(model) || "";
return this.modelDeclarationContext(model, name);
}
modelLiteral(model) {
const modelName = this.emitter.getContext().name;
reportDiagnostic(this.emitter.getProgram(), {
code: "anonymous-model",
target: model,
format: { emittedName: modelName },
});
return this.modelDeclaration(model, modelName);
}
#isInheritedProperty(property) {
const visited = [];
function isInherited(model, propertyName) {
if (visited.includes(model))
return false;
visited.push(model);
if (model.properties.has(propertyName))
return true;
if (model.baseModel === undefined)
return false;
return isInherited(model.baseModel, propertyName);
}
const model = property.model;
if (model === undefined || model.baseModel === undefined)
return false;
return isInherited(model.baseModel, property.name);
}
modelPropertyLiteral(property) {
if (isStatusCode(this.emitter.getProgram(), property))
return "";
let propertyName = ensureCSharpIdentifier(this.emitter.getProgram(), property, property.name);
const { typeReference: typeName, defaultValue: typeDefault, nullableType: nullable, } = this.#findPropertyType(property);
const doc = getDoc(this.emitter.getProgram(), property);
const attributes = new Map(getModelAttributes(this.emitter.getProgram(), property, propertyName).map((a) => [
a.type.name,
a,
]));
const modelName = this.emitter.getContext()["name"];
if (modelName === propertyName ||
(this.isDuplicateExceptionName(propertyName) &&
property.model &&
isErrorModel(this.emitter.getProgram(), property.model))) {
propertyName = `${propertyName}Prop`;
const attr = getEncodedNameAttribute(this.emitter.getProgram(), property, propertyName);
if (!attributes.has(attr.type.name))
attributes.set(attr.type.name, attr);
}
const defaultValue = this.#opHelpers.getDefaultValue(this.emitter.getProgram(), property.type, property.defaultValue) ?? typeDefault;
const attributeList = [...attributes.values()];
return this.emitter.result
.rawCode(code `${doc ? `${formatComment(doc)}\n` : ""}${`${attributeList.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributeList?.length > 0 ? "\n" : ""}`}public ${this.#isInheritedProperty(property) ? "new " : ""}${typeName}${isValueType(this.emitter.getProgram(), property.type) && (property.optional || nullable)
? "?"
: ""} ${propertyName} { get; ${typeDefault ? "}" : "set; }"}${defaultValue ? ` = ${defaultValue};\n` : "\n"}
`);
}
#findPropertyType(property) {
return this.#opHelpers.getTypeInfo(this.emitter.getProgram(), property.type, property);
}
#isMultipartRequest(operation) {
const body = operation.parameters.body;
if (body === undefined)
return false;
return body.bodyKind === "multipart";
}
#hasMultipartOperation(iface) {
for (const [_, operation] of iface.operations) {
const [httpOp, _] = getHttpOperation(this.emitter.getProgram(), operation);
if (this.#isMultipartRequest(httpOp))
return true;
}
return false;
}
modelPropertyReference(property) {
return code `${this.emitter.emitTypeReference(property.type)}`;
}
numericLiteral(number) {
return this.emitter.result.rawCode(code `${number.value.toString()}`);
}
interfaceDeclaration(iface, name) {
// interface declaration
const ifaceName = `I${ensureCSharpIdentifier(this.emitter.getProgram(), iface, name, NameCasingType.Class)}`;
const namespace = this.emitter.getContext().namespace;
const doc = getDoc(this.emitter.getProgram(), iface);
const attributes = getModelAttributes(this.emitter.getProgram(), iface, ifaceName);
this.#metadateMap.set(iface, new CSharpType({ name: ifaceName, namespace: namespace }));
const decl = this.emitter.result.declaration(ifaceName, code `
namespace ${namespace} {
${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public interface ${ifaceName} {
${this.emitter.emitInterfaceOperations(iface)}
}
} `);
return decl;
}
interfaceDeclarationContext(iface, name) {
// set up interfaces file for declaration
const ifaceName = `I${ensureCSharpIdentifier(this.emitter.getProgram(), iface, name, NameCasingType.Class)}`;
const sourceFile = this.emitter.createSourceFile(`generated/operations/${ifaceName}.cs`);
sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Interface;
sourceFile.meta["nullable"] = true;
const ifaceNamespace = this.#getOrAddNamespace(iface.namespace);
sourceFile.imports.set("System", ["System"]);
sourceFile.imports.set("System.Net", ["System.Net"]);
sourceFile.imports.set("System.Text.Json", ["System.Text.Json"]);
sourceFile.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]);
sourceFile.imports.set("System.Threading.Tasks", ["System.Threading.Tasks"]);
sourceFile.imports.set("Microsoft.AspNetCore.Mvc", ["Microsoft.AspNetCore.Mvc"]);
if (this.#hasMultipartOperation(iface)) {
sourceFile.imports.set("Microsoft.AspNetCore.WebUtilities", [
"Microsoft.AspNetCore.WebUtilities",
]);
}
return this.#createModelContext(ifaceNamespace, sourceFile, ifaceName);
}
interfaceDeclarationOperations(iface) {
// add in operations
const builder = new StringBuilder();
const context = this.emitter.getContext();
const name = `${ensureCSharpIdentifier(this.emitter.getProgram(), iface, iface.name, NameCasingType.Class)}`;
const ifaceNamespace = this.#getOrAddNamespace(iface.namespace);
const namespace = ifaceNamespace;
const mock = {
className: name,
interfaceName: `I${name}`,
methods: [],
namespace: namespace,
usings: [],
};
for (const [name, operation] of iface.operations) {
const doc = getDoc(this.emitter.getProgram(), operation);
const returnTypes = [];
const [httpOp, _] = getHttpOperation(this.emitter.getProgram(), operation);
for (const response of httpOp.responses.filter((r) => !isErrorModel(this.emitter.getProgram(), r.type))) {
const [_, responseType] = this.#resolveOperationResponse(response, httpOp.operation);
returnTypes.push(responseType);
}
const returnInfo = coalesceTypes(this.emitter.getProgram(), returnTypes, namespace);
const returnType = returnInfo?.type || UnknownType;
const opName = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Method);
const parameters = this.#opHelpers.getParameters(this.emitter.getProgram(), httpOp);
const opImpl = {
methodName: `${opName}Async`,
methodParams: `${getBusinessLogicDeclParameters(parameters)}`,
returnType: returnType,
returnTypeName: `${returnType.name === "void" ? "Task" : `Task<${returnType.getTypeReference(context.scope)}>`}`,
instantiatedReturnType: returnType.name === "void"
? undefined
: `${returnType.getTypeReference(context.scope)}`,
};
const opDecl = this.emitter.result.declaration(opName, code `${doc ? `${formatComment(doc)}\n` : ""}${opImpl.returnTypeName} ${opImpl.methodName}( ${opImpl.methodParams});`);
mock.methods.push(opImpl);
builder.push(code `${opDecl.value}\n`);
this.emitter.emitInterfaceOperation(operation);
}
mock.usings.push(...getImports(context.scope));
this.#mockRegistrations.set(mock.interfaceName, mock);
return builder.reduce();
}
interfaceOperationDeclarationContext(operation) {
const resource = getResourceOperation(this.emitter.getProgram(), operation);
const controllerName = operation.interface !== undefined
? operation.interface.name
: resource === undefined
? NoResourceContext
: resource.resourceType.name;
return this.#createOrGetResourceContext(controllerName, operation, resource?.resourceType);
}
interfaceOperationDeclaration(operation, name) {
const operationName = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Method);
const doc = getDoc(this.emitter.getProgram(), operation);
const [httpOperation, _] = getHttpOperation(this.emitter.getProgram(), operation);
const multipart = this.#isMultipartRequest(httpOperation);
const parameters = this.#opHelpers.getParameters(this.emitter.getProgram(), httpOperation);
const declParams = getHttpDeclParameters(parameters);
if (multipart) {
const context = this.emitter.getContext();
context.file.imports.set("Microsoft.AspNetCore.WebUtilities", [
"Microsoft.AspNetCore.WebUtilities",
]);
context.file.imports.set("Microsoft.AspNetCore.Http.Extensions", [
"Microsoft.AspNetCore.Http.Extensions",
]);
}
const responseInfo = this.#getOperationResponse(httpOperation);
const status = responseInfo?.statusCode ?? 200;
const response = responseInfo?.resultType ??
new CSharpType({
name: "void",
namespace: "System",
isBuiltIn: true,
isValueType: false,
});
const hasResponseValue = response.name !== "void";
const resultString = `${status === 204 ? "NoContent" : "Ok"}`;
if (!this.#isMultipartRequest(httpOperation)) {
return this.emitter.result.declaration(operation.name, code `
${doc ? `${formatComment(doc)}` : ""}
[${getOperationVerbDecorator(httpOperation)}]
[Route("${httpOperation.path}")]
${this.emitter.emitOperationReturnType(operation)}
public virtual async Task<IActionResult> ${operationName}(${declParams})
{
${hasResponseValue
? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)});
return ${resultString}(result);`
: `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)});
return ${resultString}();`}
}`);
}
else {
return this.emitter.result.declaration(operation.name, code `
${doc ? `${formatComment(doc)}` : ""}
[${getOperationVerbDecorator(httpOperation)}]
[Route("${httpOperation.path}")]
[Consumes("multipart/form-data")]
${this.emitter.emitOperationReturnType(operation)}
public virtual async Task<IActionResult> ${operationName}(${declParams})
{
var boundary = Request.GetMultipartBoundary();
if (boundary == null)
{
return BadRequest("Request missing multipart boundary");
}
var reader = new MultipartReader(boundary, Request.Body);
${hasResponseValue
? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)});
return ${resultString}(result);`
: `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)});
return ${resultString}();`}
}`);
}
}
operationDeclarationContext(operation, name) {
const resource = getResourceOperation(this.emitter.getProgram(), operation);
const controllerName = (operation.interface?.name ?? resource === undefined)
? NoResourceContext
: resource.resourceType.name;
return this.#createOrGetResourceContext(controllerName, operation, resource?.resourceType);
}
operationDeclaration(operation, name) {
const operationName = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Method);
const doc = getDoc(this.emitter.getProgram(), operation);
const [httpOperation, _] = getHttpOperation(this.emitter.getProgram(), operation);
const parameters = this.#opHelpers.getParameters(this.emitter.getProgram(), httpOperation);
const declParams = getHttpDeclParameters(parameters);
const responseInfo = this.#getOperationResponse(httpOperation);
const status = responseInfo?.statusCode ?? 200;
const response = responseInfo?.resultType ??
new CSharpType({
name: "void",
namespace: "System",
isBuiltIn: true,
isValueType: false,
});
const hasResponseValue = response.name !== "void";
const resultString = `${status === 204 ? "NoContent" : "Ok"}`;
return this.emitter.result.declaration(operation.name, code `
${doc ? `${formatComment(doc)}` : ""}
[${getOperationVerbDecorator(httpOperation)}]
[Route("${httpOperation.path}")]
${this.emitter.emitOperationReturnType(operation)}
public virtual async Task<IActionResult> ${operationName}(${declParams})
{
${hasResponseValue
? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)});
return ${resultString}(result);`
: `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)});
return ${resultString}();`}
}`);
}
operationReturnType(operation, returnType) {
const [httpOperation, _] = getHttpOperation(this.emitter.getProgram(), operation);
return this.#emitOperationResponses(httpOperation);
}
stringTemplate(stringTemplate) {
return this.emitter.result.rawCode(stringTemplate.stringValue || "");
}
#resolveOperationResponse(response, operation) {
function getName(sourceType, part) {
return ensureCSharpIdentifier(emitter.getProgram(), sourceType, part, NameCasingType.Class);
}
let responseType = new HttpMetadata().resolveLogicalResponseType(this.emitter.getProgram(), response);
if (responseType.kind === "Model" && !responseType.name) {
const modelName = `${getName(responseType, operation.interface.name)}${getName(responseType, operation.name)}Response}`;
const returnedType = this.emitter
.getProgram()
.checker.cloneType(responseType, { name: modelName });
responseType = returnedType;
}
this.emitter.emitType(responseType);
const context = this.emitter.getContext();
const result = getCSharpType(this.emitter.getProgram(), responseType, context.namespace);
return [result?.type || UnknownType, responseType];
}
#getOperationResponse(operation) {
const validResponses = operation.responses.filter((r) => !isErrorModel(this.emitter.getProgram(), r.type) &&
getCSharpStatusCode(r.statusCodes) !== undefined);
if (validResponses.length < 1)
return undefined;
const response = validResponses[0];
const csharpStatusCode = getCSharpStatusCode(response.statusCodes);
if (csharpStatusCode === undefined)
return undefined;
const responseType = new HttpMetadata().resolveLogicalResponseType(this.emitter.getProgram(), response);
const context = this.emitter.getContext();
const result = getCSharpType(this.emitter.getProgram(), responseType, context.namespace);
const resultType = result?.type || UnknownType;
return {
csharpStatusCode,
resultType,
statusCode: response.statusCodes,
};
}
#emitOperationResponses(operation) {
function isValid(program, response) {
return (!isErrorModel(program, response.type) &&
getCSharpStatusCode(response.statusCodes) !== undefined);
}
const builder = new StringBuilder();
for (const response of operation.responses) {
const [responseType, _] = this.#resolveOperationResponse(response, operation.operation);
if (isValid(this.emitter.getProgram(), response)) {
builder.push(code `${builder.segments.length > 0 ? "\n" : ""}${this.#emitOperationResponseDecorator(response, responseType)}`);
}
}
return builder.reduce();
}
#emitOperationResponseDecorator(response, result) {
return this.emitter.result.rawCode(code `[ProducesResponseType((int)${getCSharpStatusCode(response.statusCodes)}, Type = typeof(${this.#emitResponseType(result)}))]`);
}
#emitResponseType(type) {
const context = this.emitter.getContext();
return type.getTypeReference(context.scope);
}
unionDeclaration(union, name) {
const baseType = coalesceUnionTypes(this.emitter.getProgram(), union);
if (isStringEnumType(this.emitter.getProgram(), union)) {
const program = this.emitter.getProgram();
const unionName = ensureCSharpIdentifier(program, union, name);
const namespace = this.emitter.getContext().namespace;
const doc = getDoc(this.emitter.getProgram(), union);
const attributes = getModelAttributes(program, union, unionName);
this.#metadateMap.set(union, new CSharpType({ name: unionName, namespace: namespace }));
return this.emitter.result.declaration(unionName, code `
namespace ${namespace}
{
${doc ? `${formatComment(doc)}` : ""}
${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}
public enum ${unionName}
{
${this.emitter.emitUnionVariants(union)}
}
} `);
}
return this.emitter.result.rawCode(code `${baseType.getTypeReference(this.emitter.getContext().scope)}`);
}
unionDeclarationContext(union) {
if (isStringEnumType(this.emitter.getProgram(), union)) {
const unionName = ensureCSharpIdentifier(this.emitter.getProgram(), union, union.name || "Union");
const unionFile = this.emitter.createSourceFile(`generated/models/${unionName}.cs`);
unionFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model;
const unionNamespace = this.#getOrAddNamespace(union.namespace);
return this.#createEnumContext(unionNamespace, unionFile, unionName);
}
else {
return this.emitter.getContext();
}
}
unionInstantiation(union, name) {
return super.unionInstantiation(union, name);
}
unionInstantiationContext(union, name) {
return super.unionInstantiationContext(union, name);
}
unionVariants(union) {
const result = new StringBuilder();
let i = 0;
for (const [name, variant] of union.variants) {
i++;
if (variant.type.kind === "String") {
const nameHint = name || variant.type.value;
const memberName = ensureCSharpIdentifier(this.emitter.getProgram(), variant, nameHint);
this.#metadateMap.set(variant, { name: memberName });
result.push(code `
[JsonStringEnumMemberName("${variant.type.value}")]
${ensureCSharpIdentifier(this.emitter.getProgram(), variant, nameHint, NameCasingType.Property)}`);
if (i < union.variants.size)
result.pushLiteralSegment(", ");
}
}
return this.emitter.result.rawCode(result.reduce());
}
unionVariantContext(union) {
return super.unionVariantContext(union);
}
#createModelContext(namespace, file, name) {
file.imports.set("System", ["System"]);
file.imports.set("System.Text.Json", ["System.Text.Json"]);
file.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]);
file.imports.set("TypeSpec.Helpers.JsonConverters", ["TypeSpec.Helpers.JsonConverters"]);
file.imports.set("TypeSpec.Helpers", ["TypeSpec.Helpers"]);
file.meta[this.#nsKey] = namespace;
const context = {
namespace: namespace,
name: name,
file: file,
scope: file.globalScope,
};
return context;
}
#createEnumContext(namespace, file, name) {
file.imports.set("System.Text.Json", ["System.Text.Json"]);
file.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]);
return {
namespace: namespace,
name: name,
file: file,
scope: file.globalScope,
};
}
#createOrGetResourceContext(name, operation, resource) {
name = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Class);
const namespace = this.#getOrAddNamespace(operation.namespace);
let context = controllers.get(`${namespace}.${name}`);
if (context !== undefined)
return context;
const sourceFile = this.emitter.createSourceFile(`generated/controllers/${name}Controller.cs`);
sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Controller;
sourceFile.meta["resourceName"] = name;
sourceFile.meta["resource"] = `${name}Controller`;
sourceFile.meta["namespace"] = namespace;
sourceFile.imports.set("System", ["System"]);
sourceFile.imports.set("System.Net", ["System.Net"]);
sourceFile.imports.set("System.Threading.Tasks", ["System.Threading.Tasks"]);
sourceFile.imports.set("System.Text.Json", ["System.Text.Json"]);
sourceFile.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]);
sourceFile.imports.set("Microsoft.AspNetCore.Mvc", ["Microsoft.AspNetCore.Mvc"]);
sourceFile.imports.set(namespace, [namespace]);
sourceFile.meta[this.#nsKey] = namespace;
context = {
namespace: namespace,
file: sourceFile,
resourceName: name,
scope: sourceFile.globalScope,
resourceType: resource,
};
controllers.set(`${namespace}.${name}`, context);
return context;
}
// eslint-disable-next-line no-unused-private-class-members
#getNamespaceFullName(namespace) {
return namespace
? ensureCSharpIdentifier(this.emitter.getProgram(), namespace, getNamespaceFullName(namespace))
: "TypeSpec";
}
reference(targetDeclaration, pathUp, pathDown, commonScope) {
const resolved = resolveReferenceFromScopes(targetDeclaration, pathDown, pathUp);
return resolved ?? super.reference(targetDeclaration, pathUp, pathDown, commonScope);
}
scalarInstantiation(scalar, name) {
const scalarType = getCSharpTypeForScalar(this.emitter.getProgram(), scalar);
return scalarType.getTypeReference(this.emitter.getContext().scope);
}
scalarDeclaration(scalar, name) {
const scalarType = getCSharpTypeForScalar(this.emitter.getProgram(), scalar);
return scalarType.getTypeReference(this.emitter.getContext().scope);
}
sourceFile(sourceFile) {
if (sourceFile.meta.emitted) {
return sourceFile.meta.emitted;
}
const emittedSourceFile = {
path: sourceFile.path,