Repository: asyncapi/modelina (ts)

Issue:
Constants are compiled as enums
#### Describe the bug
If I have a value that's a constant on an object, it will get compiled into an anonymous schema. This overcomplicates things by a lot.

Input:
```yaml
    TradeMessageClient:
      name: Trade Message (client)
      title: Trade Message (client)
      description: A message containing trade data
      payload:
        title: TradePayloadClient
        type: object
        properties:
          operation:
            type: string
            const: Trade
          data:
            $ref: '#/components/schemas/TradeClient'
        additionalProperties: false
        required:
          - operation
          - data
```

Expected TypeScript output:
```typescript
interface TradePayloadClient {
  operation: "Trade"
  data: TradeClient;
}
```

Actual Typescript Output:
```typescript
interface TradePayloadClient {
  operation: AnonymousSchema_11;
  data: TradeClient;
}
enum AnonymousSchema_11 {
  TRADE = "Trade",
}
```

Expected C# output:
```c#
public class TradePayloadClient
{
    public const string Operation = "Trade";
    public TradeClient? Data { get; set; }
}
```

Actual C# output:
```c#

public class TradePayloadClient
  {
    public AnonymousSchema_11? Operation { get; set; }
    public TradeClient? Data { get; set; }
  }

public enum AnonymousSchema_11
  {
    TRADE
  }

  public static class AnonymousSchema_11Extensions
  {
    public static string? GetValue(this AnonymousSchema_11 enumValue)
    {
      switch (enumValue)
      {
        case AnonymousSchema_11.TRADE: return "Trade";
      }
      return null;
    }

    public static AnonymousSchema_11? ToAnonymousSchema_11(string? value)
    {
      switch (value)
      {
        case "Trade": return AnonymousSchema_11.TRADE;
      }
      return null;
    }
  }
```


Pull request description:
feat!: properly implement constants in c#
## Description
Enable the C# Class and Record Generators to properly handle constant values.

## Related Issue
fixes #1471 

## Checklist
- [x] The code follows the project's coding standards and is properly linted (`npm run lint`).
- [x] Tests have been added or updated to cover the changes.
- [ ] Documentation has been updated to reflect the changes.
- [x] All tests pass successfully locally.(`npm run test`).


Patch:
diff --git a/docs/migrations/version-3-to-4.md b/docs/migrations/version-3-to-4.md
index 8bdafbb2..2d98d6aa 100644
--- a/docs/migrations/version-3-to-4.md
+++ b/docs/migrations/version-3-to-4.md
@@ -28,4 +28,34 @@ but will now generate:
 interface AnonymousSchema_1 {
   aa_00_testAttribute?: string;
 }
-```
\ No newline at end of file
+```
+
+## C#
+
+### Constant values are now properly rendered as const properties
+
+This example used to generate a `string` with a getter and setter, but will now generate a const string that is initialized to the const value provided. 
+
+```yaml
+type: object
+properties:
+  property:
+    type: string
+    const: 'abc'
+```
+
+will generate
+
+```csharp
+public class TestClass {
+  private const string property = "test";  
+  
+  public string Property 
+  {
+    get { return property; }
+  }
+  ...
+}
+```
+
+Notice that `Property` no longer has a `set` method. This might break existing models.
\ No newline at end of file
diff --git a/src/generators/csharp/constrainer/ConstantConstrainer.ts b/src/generators/csharp/constrainer/ConstantConstrainer.ts
index 797006c4..a00d7a8a 100644
--- a/src/generators/csharp/constrainer/ConstantConstrainer.ts
+++ b/src/generators/csharp/constrainer/ConstantConstrainer.ts
@@ -1,7 +1,53 @@
+import {
+  ConstrainedEnumModel,
+  ConstrainedMetaModel,
+  ConstrainedMetaModelOptionsConst,
+  ConstrainedReferenceModel,
+  ConstrainedStringModel
+} from '../../../models';
 import { CSharpConstantConstraint } from '../CSharpGenerator';
 
+const getConstrainedEnumModelConstant = (args: {
+  constrainedMetaModel: ConstrainedMetaModel;
+  constrainedEnumModel: ConstrainedEnumModel;
+  constOptions: ConstrainedMetaModelOptionsConst;
+}) => {
+  const constrainedEnumValueModel = args.constrainedEnumModel.values.find(
+    (value) => value.originalInput === args.constOptions.originalInput
+  );
+
+  if (constrainedEnumValueModel) {
+    return `${args.constrainedMetaModel.type}.${constrainedEnumValueModel.key}`;
+  }
+};
+
 export function defaultConstantConstraints(): CSharpConstantConstraint {
-  return () => {
+  return ({ constrainedMetaModel }) => {
+    const constOptions = constrainedMetaModel.options.const;
+
+    if (!constOptions) {
+      return undefined;
+    }
+
+    if (
+      constrainedMetaModel instanceof ConstrainedReferenceModel &&
+      constrainedMetaModel.ref instanceof ConstrainedEnumModel
+    ) {
+      return getConstrainedEnumModelConstant({
+        constrainedMetaModel,
+        constrainedEnumModel: constrainedMetaModel.ref,
+        constOptions
+      });
+    } else if (constrainedMetaModel instanceof ConstrainedEnumModel) {
+      return getConstrainedEnumModelConstant({
+        constrainedMetaModel,
+        constrainedEnumModel: constrainedMetaModel,
+        constOptions
+      });
+    } else if (constrainedMetaModel instanceof ConstrainedStringModel) {
+      return `"${constOptions.originalInput}"`;
+    }
+
     return undefined;
   };
 }
diff --git a/src/generators/csharp/presets/JsonSerializerPreset.ts b/src/generators/csharp/presets/JsonSerializerPreset.ts
index 2299cfbe..3fbde99d 100644
--- a/src/generators/csharp/presets/JsonSerializerPreset.ts
+++ b/src/generators/csharp/presets/JsonSerializerPreset.ts
@@ -131,27 +131,32 @@ function renderDeserializeProperty(model: ConstrainedObjectPropertyModel) {
 
 function renderDeserializeProperties(model: ConstrainedObjectModel) {
   const propertyEntries = Object.entries(model.properties || {});
-  const deserializeProperties = propertyEntries.map(([prop, propModel]) => {
-    const pascalProp = pascalCase(prop);
-    //Unwrapped dictionary properties, need to be unwrapped in JSON
-    if (
-      propModel.property instanceof ConstrainedDictionaryModel &&
-      propModel.property.serializationType === 'unwrap'
-    ) {
-      return `if(instance.${pascalProp} == null) { instance.${pascalProp} = new Dictionary<${
-        propModel.property.key.type
-      }, ${propModel.property.value.type}>(); }
+  const deserializeProperties = propertyEntries
+    .map(([prop, propModel]) => {
+      const pascalProp = pascalCase(prop);
+      //Unwrapped dictionary properties, need to be unwrapped in JSON
+      if (
+        propModel.property instanceof ConstrainedDictionaryModel &&
+        propModel.property.serializationType === 'unwrap'
+      ) {
+        return `if(instance.${pascalProp} == null) { instance.${pascalProp} = new Dictionary<${
+          propModel.property.key.type
+        }, ${propModel.property.value.type}>(); }
       var deserializedValue = ${renderDeserializeProperty(propModel)};
       instance.${pascalProp}.Add(propertyName, deserializedValue);
       continue;`;
-    }
-    return `if (propertyName == "${propModel.unconstrainedPropertyName}")
+      }
+      if (propModel.property.options.const) {
+        return undefined;
+      }
+      return `if (propertyName == "${propModel.unconstrainedPropertyName}")
   {
     var value = ${renderDeserializeProperty(propModel)};
     instance.${pascalProp} = value;
     continue;
   }`;
-  });
+    })
+    .filter((prop): prop is string => !!prop);
   return deserializeProperties.join('\n');
 }
 
diff --git a/src/generators/csharp/presets/NewtonsoftSerializerPreset.ts b/src/generators/csharp/presets/NewtonsoftSerializerPreset.ts
index 8c803ae4..d36f3ae4 100644
--- a/src/generators/csharp/presets/NewtonsoftSerializerPreset.ts
+++ b/src/generators/csharp/presets/NewtonsoftSerializerPreset.ts
@@ -85,19 +85,24 @@ function renderDeserialize({
       !(prop.property instanceof ConstrainedDictionaryModel) ||
       prop.property.serializationType === 'normal'
   );
-  const corePropsRead = coreProps.map((prop) => {
-    const propertyAccessor = pascalCase(prop.propertyName);
-    let toValue = `jo["${prop.unconstrainedPropertyName}"].ToObject<${prop.property.type}>(serializer)`;
-    if (
-      prop.property instanceof ConstrainedReferenceModel &&
-      prop.property.ref instanceof ConstrainedEnumModel
-    ) {
-      toValue = `${prop.property.name}Extensions.To${prop.property.name}(jo["${prop.unconstrainedPropertyName}"].ToString())`;
-    }
-    return `if(jo["${prop.unconstrainedPropertyName}"] != null) {
+  const corePropsRead = coreProps
+    .map((prop) => {
+      const propertyAccessor = pascalCase(prop.propertyName);
+      let toValue = `jo["${prop.unconstrainedPropertyName}"].ToObject<${prop.property.type}>(serializer)`;
+      if (
+        prop.property instanceof ConstrainedReferenceModel &&
+        prop.property.ref instanceof ConstrainedEnumModel
+      ) {
+        toValue = `${prop.property.name}Extensions.To${prop.property.name}(jo["${prop.unconstrainedPropertyName}"].ToString())`;
+      }
+      if (prop.property.options.const) {
+        return undefined;
+      }
+      return `if(jo["${prop.unconstrainedPropertyName}"] != null) {
   value.${propertyAccessor} = ${toValue};
 }`;
-  });
+    })
+    .filter((prop): prop is string => !!prop);
   const nonDictionaryPropCheck = coreProps.map((prop) => {
     return `prop.Name != "${prop.unconstrainedPropertyName}"`;
   });
diff --git a/src/generators/csharp/renderers/ClassRenderer.ts b/src/generators/csharp/renderers/ClassRenderer.ts
index f0dc96e8..a5fc4215 100644
--- a/src/generators/csharp/renderers/ClassRenderer.ts
+++ b/src/generators/csharp/renderers/ClassRenderer.ts
@@ -115,11 +115,21 @@ export const CSHARP_DEFAULT_CLASS_PRESET: CsharpClassPreset<CSharpOptions> = {
       const getter = await renderer.runGetterPreset(property);
       const setter = await renderer.runSetterPreset(property);
 
+      if (property.property.options.const) {
+        return `public const ${property.property.type} ${pascalCase(
+          property.propertyName
+        )} { ${getter} } = ${property.property.options.const.value};`;
+      }
+
       const semiColon = nullablePropertyEnding !== '' ? ';' : '';
       return `public ${property.property.type} ${pascalCase(
         property.propertyName
       )} { ${getter} ${setter} }${nullablePropertyEnding}${semiColon}`;
     }
+
+    if (property.property.options.const) {
+      return `private const ${property.property.type} ${property.propertyName} = ${property.property.options.const.value};`;
+    }
     return `private ${property.property.type} ${property.propertyName}${nullablePropertyEnding};`;
   },
   async accessor({ renderer, options, property }) {
@@ -128,6 +138,13 @@ export const CSHARP_DEFAULT_CLASS_PRESET: CsharpClassPreset<CSharpOptions> = {
       return '';
     }
 
+    if (property.property.options.const) {
+      return `public ${property.property.type} ${formattedAccessorName} 
+{
+  ${await renderer.runGetterPreset(property)}
+}`;
+    }
+
     return `public ${property.property.type} ${formattedAccessorName} 
 {
   ${await renderer.runGetterPreset(property)}
diff --git a/src/generators/csharp/renderers/RecordRenderer.ts b/src/generators/csharp/renderers/RecordRenderer.ts
index eb2fe1c0..b04e5474 100644
--- a/src/generators/csharp/renderers/RecordRenderer.ts
+++ b/src/generators/csharp/renderers/RecordRenderer.ts
@@ -77,6 +77,11 @@ export const CSHARP_DEFAULT_RECORD_PRESET: CsharpRecordPreset<CSharpOptions> = {
   async property({ renderer, property }) {
     const getter = await renderer.runGetterPreset(property);
     const setter = await renderer.runSetterPreset(property);
+    if (property.property.options.const) {
+      return `public const ${property.property.type} ${pascalCase(
+        property.propertyName
+      )} = ${property.property.options.const.value};`;
+    }
     return `public ${property.required ? 'required ' : ''}${
       property.property.type
     } ${pascalCase(property.propertyName)} { ${getter} ${setter} }`;


Test patch:
diff --git a/test/generators/csharp/CSharpGenerator.spec.ts b/test/generators/csharp/CSharpGenerator.spec.ts
index 6bf12515..86f81aa6 100644
--- a/test/generators/csharp/CSharpGenerator.spec.ts
+++ b/test/generators/csharp/CSharpGenerator.spec.ts
@@ -148,6 +148,28 @@ describe('CSharpGenerator', () => {
     ]);
   });
 
+  test('should generate a const string in record', async () => {
+    const doc = {
+      $id: '_address',
+      type: 'object',
+      properties: {
+        property: { type: 'string', const: 'test' }
+      },
+      required: ['property'],
+      additionalProperties: {
+        type: 'string'
+      }
+    };
+
+    generator.options.modelType = 'record';
+    const models = await generator.generate(doc);
+    expect(models).toHaveLength(1);
+    expect(models[0].result).toMatchSnapshot();
+    expect(models[0].dependencies).toEqual([
+      'using System.Collections.Generic;'
+    ]);
+  });
+
   test('should render `enum` type', async () => {
     const doc = {
       $id: 'Things',
@@ -166,8 +188,7 @@ describe('CSharpGenerator', () => {
       enum: ['test+', 'test', 'test-', 'test?!', '*test']
     };
 
-    generator = new CSharpGenerator();
-
+    generator.options.modelType = 'record';
     const models = await generator.generate(doc);
     expect(models).toHaveLength(1);
     expect(models[0].result).toMatchSnapshot();
@@ -304,5 +325,28 @@ describe('CSharpGenerator', () => {
         'using System.Collections.Generic;'
       ]);
     });
+
+    test('should generate a const string', async () => {
+      const doc = {
+        $id: 'CustomClass',
+        type: 'object',
+        properties: {
+          property: { type: 'string', const: 'test' }
+        },
+        additionalProperties: {
+          type: 'string'
+        },
+        required: ['property']
+      };
+
+      generator = new CSharpGenerator();
+
+      const models = await generator.generate(doc);
+      expect(models).toHaveLength(1);
+      expect(models[0].result).toMatchSnapshot();
+      expect(models[0].dependencies).toEqual([
+        'using System.Collections.Generic;'
+      ]);
+    });
   });
 });
diff --git a/test/generators/csharp/__snapshots__/CSharpGenerator.spec.ts.snap b/test/generators/csharp/__snapshots__/CSharpGenerator.spec.ts.snap
index 995ae5e0..ca19fd43 100644
--- a/test/generators/csharp/__snapshots__/CSharpGenerator.spec.ts.snap
+++ b/test/generators/csharp/__snapshots__/CSharpGenerator.spec.ts.snap
@@ -32,6 +32,33 @@ exports[`CSharpGenerator class renderer should be able to overwrite property pre
 }"
 `;
 
+exports[`CSharpGenerator class renderer should generate a const string 1`] = `
+"public partial class CustomClass
+{
+  private const string property = \\"test\\";
+  private Dictionary<string, string>? additionalProperties;
+
+  public string Property 
+  {
+    get { return property; }
+  }
+
+  public Dictionary<string, string>? AdditionalProperties 
+  {
+    get { return additionalProperties; }
+    set { additionalProperties = value; }
+  }
+}"
+`;
+
+exports[`CSharpGenerator should generate a const string in record 1`] = `
+"public partial record Address
+{
+  public const string Property = \\"test\\";
+  public Dictionary<string, string>? AdditionalProperties { get; init; }
+}"
+`;
+
 exports[`CSharpGenerator should render \`class\` type 1`] = `
 "public partial class Address
 {
diff --git a/test/generators/csharp/presets/JsonSerializerPreset.spec.ts b/test/generators/csharp/presets/JsonSerializerPreset.spec.ts
index b7cc9448..e801b9f0 100644
--- a/test/generators/csharp/presets/JsonSerializerPreset.spec.ts
+++ b/test/generators/csharp/presets/JsonSerializerPreset.spec.ts
@@ -9,6 +9,7 @@ const doc = {
   required: ['string prop'],
   properties: {
     'string prop': { type: 'string' },
+    'const string prop': { type: 'string', const: 'abc' },
     numberProp: { type: 'number' },
     enumProp: {
       $id: 'EnumTest',
diff --git a/test/generators/csharp/presets/NewtonsoftSerializerPreset.spec.ts b/test/generators/csharp/presets/NewtonsoftSerializerPreset.spec.ts
index 08eae68d..b6633ee4 100644
--- a/test/generators/csharp/presets/NewtonsoftSerializerPreset.spec.ts
+++ b/test/generators/csharp/presets/NewtonsoftSerializerPreset.spec.ts
@@ -9,6 +9,7 @@ const doc = {
   required: ['string prop'],
   properties: {
     'string prop': { type: 'string' },
+    'const string prop': { type: 'string', const: 'abc' },
     numberProp: { type: 'number' },
     enumProp: {
       $id: 'EnumTest',
diff --git a/test/generators/csharp/presets/__snapshots__/JsonSerializerPreset.spec.ts.snap b/test/generators/csharp/presets/__snapshots__/JsonSerializerPreset.spec.ts.snap
index 21ac5765..4a55715a 100644
--- a/test/generators/csharp/presets/__snapshots__/JsonSerializerPreset.spec.ts.snap
+++ b/test/generators/csharp/presets/__snapshots__/JsonSerializerPreset.spec.ts.snap
@@ -5,6 +5,7 @@ exports[`JSON serializer preset should render serialize and deserialize converte
 public partial class Test
 {
   private string stringProp;
+  private const string? constStringProp = \\"abc\\";
   private double? numberProp;
   private EnumTest? enumProp;
   private NestedTest? objectProp;
@@ -16,6 +17,11 @@ public partial class Test
     set { stringProp = value; }
   }
 
+  public string? ConstStringProp 
+  {
+    get { return constStringProp; }
+  }
+
   public double? NumberProp 
   {
     get { return numberProp; }
@@ -119,6 +125,11 @@ internal class TestConverter : JsonConverter<Test>
       writer.WritePropertyName(\\"string prop\\");
       JsonSerializer.Serialize(writer, value.StringProp, options);
     }
+    if(value.ConstStringProp != null) {
+      // write property name and let the serializer serialize the value itself
+      writer.WritePropertyName(\\"const string prop\\");
+      JsonSerializer.Serialize(writer, value.ConstStringProp, options);
+    }
     if(value.NumberProp != null) {
       // write property name and let the serializer serialize the value itself
       writer.WritePropertyName(\\"numberProp\\");
diff --git a/test/generators/csharp/presets/__snapshots__/NewtonsoftSerializerPreset.spec.ts.snap b/test/generators/csharp/presets/__snapshots__/NewtonsoftSerializerPreset.spec.ts.snap
index 3f7da982..99660f9e 100644
--- a/test/generators/csharp/presets/__snapshots__/NewtonsoftSerializerPreset.spec.ts.snap
+++ b/test/generators/csharp/presets/__snapshots__/NewtonsoftSerializerPreset.spec.ts.snap
@@ -5,6 +5,7 @@ exports[`Newtonsoft JSON serializer preset should render serialize and deseriali
 public partial class Test
 {
   private string stringProp;
+  private const string? constStringProp = \\"abc\\";
   private double? numberProp;
   private EnumTest? enumProp;
   private NestedTest? objectProp;
@@ -16,6 +17,11 @@ public partial class Test
     set { stringProp = value; }
   }
 
+  public string? ConstStringProp 
+  {
+    get { return constStringProp; }
+  }
+
   public double? NumberProp 
   {
     get { return numberProp; }
@@ -61,7 +67,7 @@ if(jo[\\"objectProp\\"] != null) {
   value.ObjectProp = jo[\\"objectProp\\"].ToObject<NestedTest?>(serializer);
 }
 
-  var additionalProperties = jo.Properties().Where((prop) => prop.Name != \\"string prop\\" || prop.Name != \\"numberProp\\" || prop.Name != \\"enumProp\\" || prop.Name != \\"objectProp\\");
+  var additionalProperties = jo.Properties().Where((prop) => prop.Name != \\"string prop\\" || prop.Name != \\"const string prop\\" || prop.Name != \\"numberProp\\" || prop.Name != \\"enumProp\\" || prop.Name != \\"objectProp\\");
   value.AdditionalProperties = new Dictionary<string, dynamic>();
 
   foreach (var additionalProperty in additionalProperties)
@@ -78,6 +84,10 @@ if(jo[\\"objectProp\\"] != null) {
 {
   jo.Add(\\"string prop\\", JToken.FromObject(value.StringProp, serializer));
 }
+if (value.ConstStringProp != null)
+{
+  jo.Add(\\"const string prop\\", JToken.FromObject(value.ConstStringProp, serializer));
+}
 if (value.NumberProp != null)
 {
   jo.Add(\\"numberProp\\", JToken.FromObject(value.NumberProp, serializer));