All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.swagger.codegen.v3.generators.dotnet.AbstractCSharpCodegen Maven / Gradle / Ivy

There is a newer version: 1.0.54
Show newest version
package io.swagger.codegen.v3.generators.dotnet;

import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.Lambda;
import com.google.common.collect.ImmutableMap;
import io.swagger.codegen.v3.CodegenConstants;
import io.swagger.codegen.v3.CodegenContent;
import io.swagger.codegen.v3.CodegenModel;
import io.swagger.codegen.v3.CodegenOperation;
import io.swagger.codegen.v3.CodegenProperty;
import io.swagger.codegen.v3.generators.DefaultCodegenConfig;
import io.swagger.codegen.v3.generators.handlebars.csharp.CsharpHelper;
import io.swagger.codegen.v3.generators.handlebars.lambda.CamelCaseLambda;
import io.swagger.codegen.v3.generators.handlebars.lambda.IndentedLambda;
import io.swagger.codegen.v3.generators.handlebars.lambda.LowercaseLambda;
import io.swagger.codegen.v3.generators.handlebars.lambda.TitlecaseLambda;
import io.swagger.codegen.v3.generators.handlebars.lambda.UppercaseLambda;
import io.swagger.codegen.v3.generators.util.OpenAPIUtil;
import io.swagger.codegen.v3.utils.ModelUtils;
import io.swagger.codegen.v3.utils.URLPathUtil;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.DateSchema;
import io.swagger.v3.oas.models.media.DateTimeSchema;
import io.swagger.v3.oas.models.media.MapSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.parser.util.SchemaTypeUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static io.swagger.codegen.v3.CodegenConstants.IS_ENUM_EXT_NAME;
import static io.swagger.codegen.v3.generators.handlebars.ExtensionHelper.getBooleanValue;

public abstract class AbstractCSharpCodegen extends DefaultCodegenConfig {

    protected boolean optionalAssemblyInfoFlag = true;
    protected boolean optionalProjectFileFlag = true;
    protected boolean optionalEmitDefaultValue = false;
    protected boolean optionalMethodArgumentFlag = true;
    protected boolean useDateTimeOffsetFlag = false;
    protected boolean useCollection = false;
    protected boolean returnICollection = false;
    protected boolean preserveNewLines = false;
    protected boolean netCoreProjectFileFlag = false;

    protected String modelPropertyNaming = CodegenConstants.MODEL_PROPERTY_NAMING_TYPE.PascalCase.name();

    protected String packageVersion = "1.0.0";
    protected String packageName = "IO.Swagger";
    protected String packageTitle = "Swagger Library";
    protected String packageProductName = "SwaggerLibrary";
    protected String packageDescription = "A library generated from a Swagger doc";
    protected String packageCompany = "Swagger";
    protected String packageCopyright = "No Copyright";
    protected String packageAuthors = "Swagger";

    protected String interfacePrefix = "I";

    protected String sourceFolder = "src";

    // TODO: Add option for test folder output location. Nice to allow e.g. ./test instead of ./src.
    //       This would require updating relative paths (e.g. path to main project file in test project file)
    protected String testFolder = sourceFolder;

    protected Set collectionTypes;
    protected Set mapTypes;

    protected Logger LOGGER = LoggerFactory.getLogger(AbstractCSharpCodegen.class);

    public AbstractCSharpCodegen() {
        super();

        supportsInheritance = true;

        // C# does not use import mapping
        importMapping.clear();

        outputFolder = "generated-code" + File.separator + this.getName();

        collectionTypes = new HashSet(
                Arrays.asList(
                        "IList", "List",
                        "ICollection", "Collection",
                        "IEnumerable")
        );

        mapTypes = new HashSet(
                Arrays.asList("IDictionary")
        );

        setReservedWords(
                Arrays.asList(
                        // set "client" as a reserved word to avoid conflicts with IO.Swagger.Client
                        // this is a workaround and can be removed if c# api client is updated to use
                        // fully qualified name
                        "Client", "client", "parameter", "File", "List", "list",
                        // local variable names in API methods (endpoints)
                        "localVarPath", "localVarPathParams", "localVarQueryParams", "localVarHeaderParams",
                        "localVarFormParams", "localVarFileParams", "localVarStatusCode", "localVarResponse",
                        "localVarPostBody", "localVarHttpHeaderAccepts", "localVarHttpHeaderAccept",
                        "localVarHttpContentTypes", "localVarHttpContentType",
                        "localVarStatusCode", "ApiResponse", "apiresponse",
                        // C# reserved words
                        "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked",
                        "class", "const", "continue", "decimal", "default", "delegate", "do", "double", "else",
                        "enum", "event", "explicit", "extern", "false", "finally", "fixed", "float", "for",
                        "foreach", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock",
                        "long", "namespace", "new", "null", "object", "operator", "out", "override", "params",
                        "private", "protected", "public", "readonly", "ref", "return", "sbyte", "sealed",
                        "short", "sizeof", "stackalloc", "static", "string", "struct", "switch", "this", "throw",
                        "true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using",
                        "virtual", "void", "volatile", "while")
        );

        // TODO: Either include fully qualified names here or handle in DefaultCodegen via lastIndexOf(".") search
        languageSpecificPrimitives = new HashSet(
                Arrays.asList(
                        "String",
                        "string",
                        "bool?",
                        "double?",
                        "decimal?",
                        "int?",
                        "long?",
                        "float?",
                        "byte[]",
                        "ICollection",
                        "Collection",
                        "List",
                        "Dictionary",
                        "DateTime?",
                        "DateTimeOffset?",
                        "String",
                        "Boolean",
                        "Double",
                        "Int32",
                        "Int64",
                        "Float",
                        "Guid?",
                        "System.IO.Stream", // not really a primitive, we include it to avoid model import
                        "Object")
        );

        instantiationTypes.put("array", "List");
        instantiationTypes.put("list", "List");
        instantiationTypes.put("map", "Dictionary");

        // Nullable types here assume C# 2 support is not part of base
        typeMapping = new HashMap();
        typeMapping.put("string", "string");
        typeMapping.put("binary", "byte[]");
        typeMapping.put("bytearray", "byte[]");
        typeMapping.put("boolean", "bool?");
        typeMapping.put("integer", "int?");
        typeMapping.put("int", "int?");
        typeMapping.put("float", "float?");
        typeMapping.put("long", "long?");
        typeMapping.put("double", "double?");
        typeMapping.put("number", "decimal?");
        typeMapping.put("BigDecimal", "decimal?");
        typeMapping.put("datetime", "DateTime?");
        typeMapping.put("date", "DateTime?");
        typeMapping.put("file", "System.IO.Stream");
        typeMapping.put("array", "List");
        typeMapping.put("list", "List");
        typeMapping.put("map", "Dictionary");
        typeMapping.put("object", "Object");
        typeMapping.put("uuid", "Guid?");
    }

    public void setReturnICollection(boolean returnICollection) {
        this.returnICollection = returnICollection;
    }

    public void setOptionalEmitDefaultValue(boolean optionalEmitDefaultValue) {
        this.optionalEmitDefaultValue = optionalEmitDefaultValue;
    }

    public void setUseCollection(boolean useCollection) {
        this.useCollection = useCollection;
        if (useCollection) {
            typeMapping.put("array", "Collection");
            typeMapping.put("list", "Collection");

            instantiationTypes.put("array", "Collection");
            instantiationTypes.put("list", "Collection");
        }
    }

    public void setOptionalMethodArgumentFlag(boolean flag) {
        this.optionalMethodArgumentFlag = flag;
    }

    public void setNetCoreProjectFileFlag(boolean flag) {
        this.netCoreProjectFileFlag = flag;
    }

    public void useDateTimeOffset(boolean flag) {
        this.useDateTimeOffsetFlag = flag;
        if (flag) typeMapping.put("datetime", "DateTimeOffset?");
        else typeMapping.put("datetime", "DateTime?");
    }

    @Override
    public void processOpts() {
        super.processOpts();

        // {{packageVersion}}
        if (additionalProperties.containsKey(CodegenConstants.PACKAGE_VERSION)) {
            setPackageVersion((String) additionalProperties.get(CodegenConstants.PACKAGE_VERSION));
        } else {
            additionalProperties.put(CodegenConstants.PACKAGE_VERSION, packageVersion);
        }

        // {{sourceFolder}}
        if (additionalProperties.containsKey(CodegenConstants.SOURCE_FOLDER)) {
            setSourceFolder((String) additionalProperties.get(CodegenConstants.SOURCE_FOLDER));
        } else {
            additionalProperties.put(CodegenConstants.SOURCE_FOLDER, this.sourceFolder);
        }

        // {{packageName}}
        if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) {
            setPackageName((String) additionalProperties.get(CodegenConstants.PACKAGE_NAME));
        } else {
            additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName);
        }

        if (additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) {
            LOGGER.warn(String.format("%s is not used by C# generators. Please use %s", CodegenConstants.INVOKER_PACKAGE, CodegenConstants.PACKAGE_NAME));
        }

        // {{packageTitle}}
        if (additionalProperties.containsKey(CodegenConstants.PACKAGE_TITLE)) {
            setPackageTitle((String) additionalProperties.get(CodegenConstants.PACKAGE_TITLE));
        } else {
            additionalProperties.put(CodegenConstants.PACKAGE_TITLE, packageTitle);
        }

        // {{packageProductName}}
        if (additionalProperties.containsKey(CodegenConstants.PACKAGE_PRODUCTNAME)) {
            setPackageProductName((String) additionalProperties.get(CodegenConstants.PACKAGE_PRODUCTNAME));
        } else {
            additionalProperties.put(CodegenConstants.PACKAGE_PRODUCTNAME, packageProductName);
        }

        // {{packageDescription}}
        if (additionalProperties.containsKey(CodegenConstants.PACKAGE_DESCRIPTION)) {
            setPackageDescription((String) additionalProperties.get(CodegenConstants.PACKAGE_DESCRIPTION));
        } else {
            additionalProperties.put(CodegenConstants.PACKAGE_DESCRIPTION, packageDescription);
        }

        // {{packageCompany}}
        if (additionalProperties.containsKey(CodegenConstants.PACKAGE_COMPANY)) {
            setPackageCompany((String) additionalProperties.get(CodegenConstants.PACKAGE_COMPANY));
        } else {
            additionalProperties.put(CodegenConstants.PACKAGE_COMPANY, packageCompany);
        }

        // {{packageCopyright}}
        if (additionalProperties.containsKey(CodegenConstants.PACKAGE_COPYRIGHT)) {
            setPackageCopyright((String) additionalProperties.get(CodegenConstants.PACKAGE_COPYRIGHT));
        } else {
            additionalProperties.put(CodegenConstants.PACKAGE_COPYRIGHT, packageCopyright);
        }

        // {{packageAuthors}}
        if (additionalProperties.containsKey(CodegenConstants.PACKAGE_AUTHORS)) {
            setPackageAuthors((String) additionalProperties.get(CodegenConstants.PACKAGE_AUTHORS));
        } else {
            additionalProperties.put(CodegenConstants.PACKAGE_AUTHORS, packageAuthors);
        }

        // {{useDateTimeOffset}}
        if (additionalProperties.containsKey(CodegenConstants.USE_DATETIME_OFFSET)) {
            useDateTimeOffset(convertPropertyToBooleanAndWriteBack(CodegenConstants.USE_DATETIME_OFFSET));
        } else {
            additionalProperties.put(CodegenConstants.USE_DATETIME_OFFSET, useDateTimeOffsetFlag);
        }

        if (additionalProperties.containsKey(CodegenConstants.USE_COLLECTION)) {
            setUseCollection(convertPropertyToBooleanAndWriteBack(CodegenConstants.USE_COLLECTION));
        } else {
            additionalProperties.put(CodegenConstants.USE_COLLECTION, useCollection);
        }

        if (additionalProperties.containsKey(CodegenConstants.RETURN_ICOLLECTION)) {
            setReturnICollection(convertPropertyToBooleanAndWriteBack(CodegenConstants.RETURN_ICOLLECTION));
        } else {
            additionalProperties.put(CodegenConstants.RETURN_ICOLLECTION, returnICollection);
        }

        if (additionalProperties.containsKey(CodegenConstants.OPTIONAL_EMIT_DEFAULT_VALUES)) {
            setOptionalEmitDefaultValue(convertPropertyToBooleanAndWriteBack(CodegenConstants.OPTIONAL_EMIT_DEFAULT_VALUES));
        } else {
            additionalProperties.put(CodegenConstants.OPTIONAL_EMIT_DEFAULT_VALUES, optionalEmitDefaultValue);
        }

        if (additionalProperties.containsKey(CodegenConstants.NETCORE_PROJECT_FILE)) {
            setNetCoreProjectFileFlag(convertPropertyToBooleanAndWriteBack(CodegenConstants.NETCORE_PROJECT_FILE));
        } else {
            additionalProperties.put(CodegenConstants.NETCORE_PROJECT_FILE, netCoreProjectFileFlag);
        }

        if (additionalProperties.containsKey(CodegenConstants.PRESERVE_COMMENT_NEWLINES)) {
            setPreserveNewLines(Boolean.valueOf(additionalProperties.get(CodegenConstants.PRESERVE_COMMENT_NEWLINES).toString()));
        }

        if (additionalProperties.containsKey(CodegenConstants.INTERFACE_PREFIX)) {
            String useInterfacePrefix = additionalProperties.get(CodegenConstants.INTERFACE_PREFIX).toString();
            if("false".equals(useInterfacePrefix.toLowerCase())) {
                setInterfacePrefix("");
            } else if(!"true".equals(useInterfacePrefix.toLowerCase())) {
                // NOTE: if user passes "true" explicitly, we use the default I- prefix. The other supported case here is a custom prefix.
                setInterfacePrefix(sanitizeName(useInterfacePrefix));
            }
        }

        // This either updates additionalProperties with the above fixes, or sets the default if the option was not specified.
        additionalProperties.put(CodegenConstants.INTERFACE_PREFIX, interfacePrefix);

        addHandlebarsLambdas(additionalProperties);
    }

    private void addHandlebarsLambdas(Map objs) {


        Map lambdas = new ImmutableMap.Builder()
                .put("lowercase", new LowercaseLambda().generator(this))
                .put("uppercase", new UppercaseLambda())
                .put("titlecase", new TitlecaseLambda())
                .put("camelcase", new CamelCaseLambda().generator(this))
                .put("camelcase_param", new CamelCaseLambda().generator(this).escapeAsParamName(true))
                .put("indented", new IndentedLambda())
                .put("indented_8", new IndentedLambda(8, " "))
                .put("indented_12", new IndentedLambda(12, " "))
                .put("indented_16", new IndentedLambda(16, " "))
                .build();


        if (objs.containsKey("lambda")) {
            LOGGER.warn("An property named 'lambda' already exists. Mustache lambdas renamed from 'lambda' to '_lambda'. " +
                    "You'll likely need to use a custom template, " +
                    "see https://github.com/swagger-api/swagger-codegen#modifying-the-client-library-format. ");
            objs.put("_lambda", lambdas);
        } else {
            objs.put("lambda", lambdas);
        }
    }

    @Override
    public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
        super.postProcessModelProperty(model, property);
    }

    @Override
    public Map postProcessModels(Map objs) {
        List models = (List) objs.get("models");
        for (Object _mo : models) {
            Map mo = (Map) _mo;
            CodegenModel cm = (CodegenModel) mo.get("model");
            for (CodegenProperty var : cm.vars) {
                // check to see if model name is same as the property name
                // which will result in compilation error
                // if found, prepend with _ to workaround the limitation
                if (var.name.equalsIgnoreCase(cm.name)) {
                    var.name = "_" + var.name;
                }
            }
        }
        // process enum in models
        return postProcessModelsEnum(objs);
    }

    /**
     * Invoked by {DefaultGenerator} after all models have been post-processed, allowing for a last pass of codegen-specific model cleanup.
     *
     * @param objs Current state of codegen object model.
     * @return An in-place modified state of the codegen object model.
     */
    @Override
    public Map postProcessAllModels(Map objs) {
        final Map processed =  super.postProcessAllModels(objs);
        postProcessEnumRefs(processed);
        return processed;
    }

    /**
     * C# differs from other languages in that Enums are not _true_ objects; enums are compiled to integral types.
     * So, in C#, an enum is considers more like a user-defined primitive.
     *
     * When working with enums, we can't always assume a RefModel is a nullable type (where default(YourType) == null),
     * so this post processing runs through all models to find RefModel'd enums. Then, it runs through all vars and modifies
     * those vars referencing RefModel'd enums to work the same as inlined enums rather than as objects.
     * @param models processed models to be further processed for enum references
     */
    @SuppressWarnings({ "unchecked" })
    private void postProcessEnumRefs(final Map models) {
        Map enumRefs = new HashMap();
        for (Map.Entry entry : models.entrySet()) {
            CodegenModel model = ModelUtils.getModelByName(entry.getKey(), models);
            boolean isEnum = getBooleanValue(model, IS_ENUM_EXT_NAME);
            if (isEnum) {
                enumRefs.put(entry.getKey(), model);
            }
        }

        for (Map.Entry entry : models.entrySet()) {
            String swaggerName = entry.getKey();
            CodegenModel model = ModelUtils.getModelByName(swaggerName, models);
            if (model != null) {
                for (CodegenProperty var : model.allVars) {
                    if (enumRefs.containsKey(var.datatype)) {
                        // Handle any enum properties referred to by $ref.
                        // This is different in C# than most other generators, because enums in C# are compiled to integral types,
                        // while enums in many other languages are true objects.
                        CodegenModel refModel = enumRefs.get(var.datatype);
                        var.allowableValues = new HashMap<>(refModel.allowableValues);
                        updateCodegenPropertyEnum(var);

                        // We do these after updateCodegenPropertyEnum to avoid generalities that don't mesh with C#.
                        var.getVendorExtensions().put(CodegenConstants.IS_PRIMITIVE_TYPE_EXT_NAME, Boolean.TRUE);
                    }
                }

                // We're looping all models here.
                if (getBooleanValue(model, CodegenConstants.IS_ENUM_EXT_NAME)) {
                    // We now need to make allowableValues.enumVars look like the context of CodegenProperty
                    Boolean isString = false;
                    Boolean isInteger = false;
                    Boolean isLong = false;
                    Boolean isByte = false;

                    if (model.dataType.startsWith("byte")) {
                        // C# Actually supports byte and short enums, swagger spec only supports byte.
                        isByte = true;
                        model.vendorExtensions.put("x-enum-byte", true);
                    } else if (model.dataType.startsWith("int32")) {
                        isInteger = true;
                        model.vendorExtensions.put("x-enum-integer", true);
                    } else if (model.dataType.startsWith("int64")) {
                        isLong = true;
                        model.vendorExtensions.put("x-enum-long", true);
                    } else {
                        // C# doesn't support non-integral enums, so we need to treat everything else as strings (e.g. to not lose precision or data integrity)
                        isString = true;
                        model.vendorExtensions.put("x-enum-string", true);
                    }

                    // Since we iterate enumVars for modelnnerEnum and enumClass templates, and CodegenModel is missing some of CodegenProperty's properties,
                    // we can take advantage of Mustache's contextual lookup to add the same "properties" to the model's enumVars scope rather than CodegenProperty's scope.
                    List> enumVars = (ArrayList>)model.allowableValues.get("enumVars");
                    List> newEnumVars = new ArrayList>();
                    for (Map enumVar : enumVars) {
                        Map mixedVars = new HashMap();
                        mixedVars.putAll(enumVar);

                        mixedVars.put("isString", isString);
                        mixedVars.put("isLong", isLong);
                        mixedVars.put("isInteger", isInteger);
                        mixedVars.put("isByte", isByte);

                        newEnumVars.add(mixedVars);
                    }

                    if (!newEnumVars.isEmpty()) {
                        model.allowableValues.put("enumVars", newEnumVars);
                    }
                }
            } else {
                LOGGER.warn("Expected to retrieve model %s by name, but no model was found. Check your -Dmodels inclusions.", swaggerName);
            }
        }
    }

    /**
     * Update codegen property's enum by adding "enumVars" (with name and value)
     *
     * @param var list of CodegenProperty
     */
    @Override
    public void updateCodegenPropertyEnum(CodegenProperty var) {
        if (var.vendorExtensions == null) {
            var.vendorExtensions = new HashMap<>();
        }

        super.updateCodegenPropertyEnum(var);

        // Because C# uses nullable primitives for datatype, and datatype is used in DefaultCodegen for determining enum-ness, guard against weirdness here.
        if (getBooleanValue(var, CodegenConstants.IS_ENUM_EXT_NAME)) {
            if ("byte".equals(var.dataFormat)) {// C# Actually supports byte and short enums.
                var.vendorExtensions.put("x-enum-byte", true);
                var.vendorExtensions.put(CodegenConstants.IS_STRING_EXT_NAME, Boolean.FALSE);
                var.vendorExtensions.put(CodegenConstants.IS_LONG_EXT_NAME, Boolean.FALSE);
                var.vendorExtensions.put(CodegenConstants.IS_INTEGER_EXT_NAME, Boolean.FALSE);
            } else if ("int32".equals(var.dataFormat)) {
                var.vendorExtensions.put(CodegenConstants.IS_INTEGER_EXT_NAME, Boolean.TRUE);
                var.vendorExtensions.put(CodegenConstants.IS_STRING_EXT_NAME, Boolean.FALSE);
                var.vendorExtensions.put(CodegenConstants.IS_LONG_EXT_NAME, Boolean.FALSE);
            } else if ("int64".equals(var.dataFormat)) {
                var.vendorExtensions.put(CodegenConstants.IS_LONG_EXT_NAME, Boolean.TRUE);
                var.vendorExtensions.put(CodegenConstants.IS_STRING_EXT_NAME, Boolean.FALSE);
                var.vendorExtensions.put(CodegenConstants.IS_INTEGER_EXT_NAME, Boolean.FALSE);
            } else {// C# doesn't support non-integral enums, so we need to treat everything else as strings (e.g. to not lose precision or data integrity)
                var.vendorExtensions.put(CodegenConstants.IS_STRING_EXT_NAME, Boolean.TRUE);
                var.vendorExtensions.put(CodegenConstants.IS_LONG_EXT_NAME, Boolean.FALSE);
                var.vendorExtensions.put(CodegenConstants.IS_INTEGER_EXT_NAME, Boolean.FALSE);
            }
        }
    }

    @Override
    public Map postProcessOperations(Map objs) {
        super.postProcessOperations(objs);
        if (objs != null) {
            boolean hasAuthMethods = false;
            Map operations = (Map) objs.get("operations");
            if (operations != null) {
                List ops = (List) operations.get("operation");
                for (CodegenOperation operation : ops) {

                    // Check return types for collection
                    if (operation.returnType != null) {
                        String typeMapping;
                        int namespaceEnd = operation.returnType.lastIndexOf(".");
                        if (namespaceEnd > 0) {
                            typeMapping = operation.returnType.substring(namespaceEnd);
                        } else {
                            typeMapping = operation.returnType;
                        }

                        if (this.collectionTypes.contains(typeMapping)) {
                            operation.getVendorExtensions().put(CodegenConstants.IS_LIST_CONTAINER_EXT_NAME, Boolean.TRUE);
                            operation.returnContainer = operation.returnType;
                            if (this.returnICollection && (
                                    typeMapping.startsWith("List") ||
                                            typeMapping.startsWith("Collection"))) {
                                // NOTE: ICollection works for both List and Collection
                                int genericStart = typeMapping.indexOf("<");
                                if (genericStart > 0) {
                                    operation.returnType = "ICollection" + typeMapping.substring(genericStart);
                                }
                            }
                        } else {
                            operation.returnContainer = operation.returnType;
                            operation.getVendorExtensions().put(CodegenConstants.IS_MAP_CONTAINER_EXT_NAME, this.mapTypes.contains(typeMapping));
                        }
                    }

                    if (operation.examples != null){
                        for (Map example : operation.examples)
                        {
                            for (Map.Entry entry : example.entrySet())
                            {
                                // Replace " with \", \r, \n with \\r, \\n
                                String val = entry.getValue().replace("\"", "\\\"")
                                        .replace("\r","\\r")
                                        .replace("\n","\\n");
                                entry.setValue(val);
                            }
                        }
                    }

                    processOperation(operation);
                    if (getBooleanValue(operation, CodegenConstants.HAS_AUTH_METHODS_EXT_NAME)) {
                        hasAuthMethods = true;
                    }
                }
            }
            objs.put("hasAuthMethods", hasAuthMethods);
        }

        return objs;
    }

    protected void processOperation(CodegenOperation operation) {
        // default noop
    }

    @Override
    public String apiFileFolder() {
        return outputFolder + File.separator + sourceFolder + File.separator + packageName + File.separator + apiPackage();
    }

    @Override
    public String modelFileFolder() {
        return outputFolder + File.separator + sourceFolder + File.separator + packageName + File.separator + modelPackage();
    }

    @Override
    public String toModelFilename(String name) {
        // should be the same as the model name
        return toModelName(name);
    }

    @Override
    public String toOperationId(String operationId) {
        // throw exception if method name is empty (should not occur as an auto-generated method name will be used)
        if (StringUtils.isEmpty(operationId)) {
            throw new RuntimeException("Empty method name (operationId) not allowed");
        }

        // method name cannot use reserved keyword, e.g. return
        if (isReservedWord(operationId)) {
            LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + camelize(sanitizeName("call_" + operationId)));
            operationId = "call_" + operationId;
        }

        return camelize(sanitizeName(operationId));
    }

    @Override
    public String toVarName(String name) {
        // sanitize name
        name = sanitizeName(name);

        // if it's all uppper case, do nothing
        if (name.matches("^[A-Z_]*$")) {
            return name;
        }

        // camelize the variable name
        // pet_id => PetId
        name = camelize(name);

        // for reserved word or word starting with number, append _
        if (isReservedWord(name) || name.matches("^\\d.*")) {
            name = escapeReservedWord(name);
        }

        return name;
    }

    @Override
    public String toParamName(String name) {
        // sanitize name
        name = sanitizeName(name);

        // replace - with _ e.g. created-at => created_at
        name = name.replaceAll("-", "_");

        // if it's all uppper case, do nothing
        if (name.matches("^[A-Z_]*$")) {
            return name;
        }

        // camelize(lower) the variable name
        // pet_id => petId
        name = camelize(name, true);

        // for reserved word or word starting with number, append _
        if (isReservedWord(name) || name.matches("^\\d.*")) {
            name = escapeReservedWord(name);
        }

        return name;
    }

    @Override
    public String escapeReservedWord(String name) {
        if(this.reservedWordsMappings().containsKey(name)) {
            return this.reservedWordsMappings().get(name);
        }
        return "_" + name;
    }

    /**
     * Return the example value of the property
     *
     * @param schema Open API Schema object
     * @return string presentation of the example value of the property
     */
    @Override
    public String toExampleValue(Schema schema) {
        if (schema instanceof StringSchema) {
            if (schema.getExample() != null) {
                return String.format("\"%s\"", schema.getExample().toString());
            }
        }
        if (schema instanceof DateSchema || schema instanceof DateTimeSchema) {
            // TODO still...
            return null;
        } else {
            if (schema.getExample() != null) {
                return schema.getExample().toString();
            }
        }
        return null;
    }

    /**
     * Return the default value of the property
     *
     * @param schema Schema object
     * @return string presentation of the default value of the property
     */
    @Override
    public String toDefaultValue(Schema schema) {
        if (schema instanceof StringSchema) {
            if (schema.getDefault() != null) {
                String _default = schema.getDefault().toString();
                if (schema.getEnum() == null) {
                    return String.format("\"%s\"", _default);
                } else {
                    // convert to enum var name later in postProcessModels
                    return _default;
                }
            }
        }
        if (schema instanceof DateSchema || schema instanceof DateTimeSchema) {
            // TODO still...
            return null;
        } else {
            if (schema.getDefault() != null) {
                if(SchemaTypeUtil.INTEGER64_FORMAT.equals(schema.getFormat())) {
                    return String.format("%1$sF", schema.getDefault());
                }
                return schema.getDefault().toString();
            }
        }
        return null;
    }

    @Override
    protected boolean isReservedWord(String word) {
        // NOTE: This differs from super's implementation in that C# does _not_ want case insensitive matching.
        return reservedWords.contains(word.toLowerCase());
    }

    @Override
    public String getSchemaType(Schema propertySchema) {
        String swaggerType = super.getSchemaType(propertySchema);

        swaggerType = getRefSchemaTargetType(propertySchema, swaggerType);

        String type;

        if (swaggerType == null) {
            swaggerType = "object";
        }

        // TODO avoid using toLowerCase as typeMapping should be case-sensitive
        if (typeMapping.containsKey(swaggerType.toLowerCase())) {
            type = typeMapping.get(swaggerType.toLowerCase());
            if (languageSpecificPrimitives.contains(type)) {
                return type;
            }
        } else {
            type = swaggerType;
        }
        return toModelName(type);
    }

    protected String getRefSchemaTargetType(Schema schema, String schemaType) {
        if (schemaType == null) {
            return null;
        }
        if (schema != null && schema.get$ref() != null) {
            final Schema refSchema = OpenAPIUtil.getSchemaFromName(schemaType, this.openAPI);
            if (refSchema != null && !isObjectSchema(refSchema) && !(refSchema instanceof ArraySchema || refSchema instanceof MapSchema) && refSchema.getEnum() == null) {
                schemaType = super.getSchemaType(refSchema);
            }
        }
        return schemaType;

    }

    /**
     * Provides C# strongly typed declaration for simple arrays of some type and arrays of arrays of some type.
     * @param arr The input array property
     * @return The type declaration when the type is an array of arrays.
     */
    private String getArrayTypeDeclaration(ArraySchema arr) {
        // TODO: collection type here should be fully qualified namespace to avoid model conflicts
        // This supports arrays of arrays.
        String arrayType = typeMapping.get("array");
        StringBuilder instantiationType = new StringBuilder(arrayType);
        Schema items = arr.getItems();
        String nestedType = getTypeDeclaration(items);
        // TODO: We may want to differentiate here between generics and primitive arrays.
        instantiationType.append("<").append(nestedType).append(">");
        return instantiationType.toString();
    }

    @Override
    public String toInstantiationType(Schema schema) {
        if (schema instanceof ArraySchema) {
            return getArrayTypeDeclaration((ArraySchema) schema);
        }
        return super.toInstantiationType(schema);
    }

    @Override
    public String getTypeDeclaration(Schema propertySchema) {
        if (propertySchema instanceof ArraySchema) {
            Schema inner = ((ArraySchema) propertySchema).getItems();
            return String.format("%s<%s>", getSchemaType(propertySchema), getTypeDeclaration(inner));
        } else if (propertySchema instanceof MapSchema && hasSchemaProperties(propertySchema)) {
            Schema inner = (Schema) propertySchema.getAdditionalProperties();
            return String.format("%s", getSchemaType(propertySchema), getTypeDeclaration(inner));
        } else if (propertySchema instanceof MapSchema && hasTrueAdditionalProperties(propertySchema)) {
            Schema inner = new ObjectSchema();
            return String.format("%s", getSchemaType(propertySchema), getTypeDeclaration(inner));
        }

        return super.getTypeDeclaration(propertySchema);
    }

    @Override
    public String toModelName(String name) {
        // We need to check if import-mapping has a different model for this class, so we use it
        // instead of the auto-generated one.
        if (importMapping.containsKey(name)) {
            return importMapping.get(name);
        }
        if (!StringUtils.isEmpty(modelNamePrefix)) {
            name = modelNamePrefix + "_" + name;
        }

        if (!StringUtils.isEmpty(modelNameSuffix)) {
            name = name + "_" + modelNameSuffix;
        }

        name = sanitizeName(name);

        // model name cannot use reserved keyword, e.g. return
        if (isReservedWord(name)) {
            LOGGER.warn(name + " (reserved word) cannot be used as model name. Renamed to " + camelize("model_" + name));
            name = "model_" + name; // e.g. return => ModelReturn (after camelize)
        }

        // model name starts with number
        if (name.matches("^\\d.*")) {
            LOGGER.warn(name + " (model name starts with number) cannot be used as model name. Renamed to " + camelize("model_" + name));
            name = "model_" + name; // e.g. 200Response => Model200Response (after camelize)
        }

        // camelize the model name
        // phone_number => PhoneNumber
        return camelize(name);
    }

    @Override
    public String apiTestFileFolder() {
        return outputFolder + ".Test";
    }

    @Override
    public String modelTestFileFolder() {
        return outputFolder + ".Test";
    }

    @Override
    public String toApiTestFilename(String name) {
        return toApiName(name) + "Tests";
    }

    @Override
    public String toModelTestFilename(String name) {
        return toModelName(name) + "Tests";
    }

    public void setPackageName(String packageName) {
        this.packageName = packageName;
    }

    public void setPackageVersion(String packageVersion) {
        this.packageVersion = packageVersion;
    }

    public void setPackageTitle(String packageTitle) {
        this.packageTitle = packageTitle;
    }

    public void setPackageProductName(String packageProductName) {
        this.packageProductName = packageProductName;
    }

    public void setPackageDescription(String packageDescription) {
        this.packageDescription = packageDescription;
    }

    public void setPackageCompany(String packageCompany) {
        this.packageCompany = packageCompany;
    }

    public void setPackageCopyright(String packageCopyright) {
        this.packageCopyright = packageCopyright;
    }

    public void setPackageAuthors(String packageAuthors) {
        this.packageAuthors = packageAuthors;
    }

    public void setSourceFolder(String sourceFolder) {
        this.sourceFolder = sourceFolder;
    }

    public String getInterfacePrefix() {
        return interfacePrefix;
    }

    public void setInterfacePrefix(final String interfacePrefix) {
        this.interfacePrefix = interfacePrefix;
    }

    public CodegenModel fromModel(String name, Schema schema, Map allDefinitions) {
        final CodegenModel codegenModel = super.fromModel(name, schema, allDefinitions);
        if (typeMapping.containsKey(name.toLowerCase()) && isReservedWord(name.toLowerCase())) {
            typeMapping.remove(name.toLowerCase());
        }
        return codegenModel;
    }

    @Override
    public String toEnumValue(String value, String datatype) {
        if (value == null) {
            return null;
        }
        // C# only supports enums as literals for int, int?, long, long?, byte, and byte?. All else must be treated as strings.
        // Per: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/enum
        // The approved types for an enum are byte, sbyte, short, ushort, int, uint, long, or ulong.
        // but we're not supporting unsigned integral types or shorts.
        if(datatype.startsWith("int") || datatype.startsWith("long") || datatype.startsWith("byte")) {
            return value;
        }

        return escapeText(value);
    }

    @Override
    public String toEnumVarName(String name, String datatype) {
        if (name.length() == 0) {
            return "Empty";
        }

        // for symbol, e.g. $, #
        if (getSymbolName(name) != null) {
            return camelize(getSymbolName(name));
         }

        if (NumberUtils.isNumber(name)) {
            return "NUMBER_" + name.replaceAll("-", "MINUS_")
                    .replaceAll("\\+", "PLUS_")
                    .replaceAll("\\.", "_DOT_");
        }

        String enumName = sanitizeName(name);

        enumName = enumName.replaceFirst("^_", "");
        enumName = enumName.replaceFirst("_$", "");

        enumName = camelize(enumName) + "Enum";

        if (enumName.matches("\\d.*")) { // starts with number
            return "_" + enumName;
        } else {
            return enumName;
        }
    }

    @Override
    public String toEnumName(CodegenProperty property) {
        return sanitizeName(camelize(property.name)) + "Enum";
    }

    public String testPackageName() {
        return this.packageName + ".Test";
    }

    public boolean isPreserveNewLines() {
        return preserveNewLines;
    }

    public void setPreserveNewLines(boolean preserveNewLines) {
        this.preserveNewLines = preserveNewLines;
    }

    @Override
    public void preprocessOpenAPI(OpenAPI openAPI) {
        super.preprocessOpenAPI(openAPI);

        final URL urlInfo = URLPathUtil.getServerURL(openAPI);
        if ( urlInfo != null && urlInfo.getPort() > 0) {
            additionalProperties.put("serverUrl", String.format("%s://%s:%s", urlInfo.getProtocol(), urlInfo.getHost(), urlInfo.getPort()));

            if (StringUtils.isNotBlank(urlInfo.getPath())) {
                additionalProperties.put("basePathWithoutHost", urlInfo.getPath());
            }
        } else {
            additionalProperties.put("serverUrl", URLPathUtil.LOCAL_HOST);
        }

        if (this.preserveNewLines) {
            Map schemaMap = openAPI.getComponents() != null ? openAPI.getComponents().getSchemas() : null;
            if (schemaMap != null) {
                for (String name : schemaMap.keySet()) {
                    Schema schema = schemaMap.get(name);
                    if (StringUtils.isNotBlank(schema.getDescription())) {
                        schema.setDescription(preserveNewlines(schema.getDescription(), 1));
                    }
                    Map propertiesMap = schema.getProperties();
                    if (propertiesMap!= null && !propertiesMap.isEmpty()) {

                        for (String propertyName : propertiesMap.keySet()) {
                            Schema propertySchema = propertiesMap.get(propertyName);
                            if (StringUtils.isNotBlank(propertySchema.getDescription())) {
                                propertySchema.setDescription(preserveNewlines(propertySchema.getDescription(), 2));
                            }
                        }
                    }
                }
            }
            for (String pathname : openAPI.getPaths().keySet()) {
                PathItem path = openAPI.getPaths().get(pathname);
                for (Operation op : path.readOperations()) {
                    if (StringUtils.isNotBlank(op.getDescription())) {
                        op.setDescription(preserveNewlines(op.getDescription(), 2));
                    }
                    if (StringUtils.isNotBlank(op.getSummary())) {
                        op.setSummary(preserveNewlines(op.getSummary(), 2));
                    }
                    if (op.getParameters() != null) {
                        for (Parameter param : op.getParameters()) {
                            if (StringUtils.isNotBlank(param.getDescription())) {
                                param.setDescription(preserveNewlines(param.getDescription(), 2));
                            }
                        }
                    }
                    if (op.getResponses() != null) {
                        for (String responseCode : op.getResponses().keySet()) {
                            ApiResponse response = op.getResponses().get(responseCode);

                            if (StringUtils.isNotBlank(response.getDescription())) {
                                response.setDescription(preserveNewlines(response.getDescription(), 2));
                            }
                        }
                    }
                }
            }
        }
    }

    @Override
    public String getDefaultTemplateDir() {
        return getName();
    }

    public String preserveNewlines(String input, int tabstops) {
        if (tabstops == 1) {
            return input.replaceAll("\\n", "~~N1");
        } else {
            // assume 2 tabstops
            return input.replaceAll("\\n", "~~N2");
        }
    }

    @Override
    public String escapeQuotationMark(String input) {
        // remove " to avoid code injection
        return input.replace("\"", "");
    }

    @Override
    public String escapeUnsafeCharacters(String input) {
        String intermediate = input.replace("*/", "*_/").replace("/*", "/_*").replace("--", "- -");

        intermediate = intermediate.replaceAll("~~N1", "\n    /// ");
        intermediate = intermediate.replaceAll("~~N2", "\n        /// ");

        return intermediate;
    }

    @Override
    public void addHandlebarHelpers(Handlebars handlebars) {
        super.addHandlebarHelpers(handlebars);
        handlebars.registerHelpers(new CsharpHelper());
    }

    @Override
    protected void addCodegenContentParameters(CodegenOperation codegenOperation, List codegenContents) {
        for (CodegenContent content : codegenContents) {
            if (content.getIsForm()) {
                addParameters(content, codegenOperation.formParams);
            } else {
                addParameters(content, codegenOperation.bodyParams);
            }
            addParameters(content, codegenOperation.headerParams);
            addParameters(content, codegenOperation.queryParams);
            addParameters(content, codegenOperation.pathParams);
        }
    }

    @Override
    public boolean checkAliasModel() {
        return true;
    }
/*
    TODO: uncomment if/when switching to stream for file upload
    @Override
    public void postProcessParameter(CodegenParameter parameter) {
        if (parameter.getIsBinary()) {
            parameter.dataType = "System.IO.Stream";
        }
        super.postProcessParameter(parameter);
    }
*/

}