簡體   English   中英

如何在 TypeScript 項目中重用現有的 C# 類定義

[英]How to reuse existing C# class definitions in TypeScript projects

我將開始在我的 HTML 客戶端項目中使用TypeScript ,該項目屬於一個已經存在實體框架域模型的 MVC 項目。 我希望我的兩個項目(客戶端和服務器端)完全分開,因為兩個團隊將致力於此……JSON 和 REST 用於來回傳遞對象。

當然,我在客戶端的domain對象應該與服務器端的對象相匹配。 過去,我通常手動完成此操作。 有沒有辦法重用我的 C# 類定義(特別是我的域模型中的POJO類)來在 TypeScript 中創建相應的類”?

目前沒有任何東西可以將 C# 映射到 TypeScript。 如果您有很多 POCO 或者您認為它們可能經常更改,您可以創建一個轉換器 - 一些簡單的......

public class MyPoco {
    public string Name { get; set; }
}

export class MyPoco {
    public Name: string;
}

在 Codeplex 上也有關於從 C# 自動生成的討論。

為了保持更新,TypeLite 可以從 C# 生成 TypeScript 接口:

http://type.litesolutions.net/

Web Essentials允許在保存時將 C# 文件編譯為 TypeScript .d.ts文件。 然后您可以從.ts文件中引用定義。

在此處輸入圖像描述

如果你使用 vscode,你可以使用我的擴展csharp2ts ,它就是這樣做的。

您只需選擇粘貼的 C# 代碼並從命令面板運行Convert C# to TypeScript命令在此處輸入圖像描述 轉換示例:

public class Person
{
    /// <summary>
    /// Primary key
    /// </summary>
    public int Id { get; set; }

    /// <summary>
    /// Person name
    /// </summary>
    public string Name { get; set; }
}

export interface Person
{
    /**Primary key */
    Id : number;

    /**Person name */
    Name : string;
}

上面的 TypeLite 和 T4TSs 看起來都不錯,只是選擇了一個,TypeLite,將其分叉以獲得對

  • 值類型
  • 可為空
  • camelCasing (TypeScript 根文檔使用駝峰,這與 C# 一起使用太好了)
  • 公共領域(喜歡干凈和可讀的 POCO,也使 C# 編譯器更容易)
  • 禁用模塊生成

然后我需要 C#接口並認為是時候烘焙我自己的東西並編寫了一個簡單的 T4 腳本來滿足我的需要。 它還包括枚舉 不需要回購,只需 < 100 行 T4。

用法
沒有庫,沒有 NuGet,只有這個簡單的 T4 文件——在 Visual Studio 中使用“添加項目”並選擇任何 T4 模板。 然后將其粘貼到文件中。 用“ACME”調整每一行。 為每個 C# 類添加一行

<#= Interface<Acme.Duck>() #>

順序很重要,任何已知類型都將用於以下接口。 如果你只使用接口,文件擴展名可以是.d.ts ,對於枚舉你需要一個.ts文件,因為變量被實例化了。

客制化
破解腳本。

<#@ template debug="true" hostSpecific="true" language="C#" #>
<#@ output extension=".ts" #>
<#@ Assembly Name="System.Core.dll" #>
<#@ assembly name="$(TargetDir)ACME.Core.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Linq" #>

<#= Interface<Acme.Bunny>() #>
<#= Interface<Acme.Duck>() #>
<#= Interface<Acme.Birdy>() #>
<#= Enums<Acme.CarrotGrade>() #>
<#= Interface<Acme.LinkParticle>() #>

<#+  
    List<Type> knownTypes = new List<Type>();

    string Interface<T>()
    {   
        Type t = typeof(T);     
        var sb = new StringBuilder();
        sb.AppendFormat("interface {0} {{\n", t.Name);
        foreach (var mi in GetInterfaceMembers(t))
        {
            sb.AppendFormat("  {0}: {1};\n", this.ToCamelCase(mi.Name), GetTypeName(mi));
        }
        sb.AppendLine("}");
        knownTypes.Add(t);
        return sb.ToString();
    }

    IEnumerable<MemberInfo> GetInterfaceMembers(Type type)
    {
        return type.GetMembers(BindingFlags.Public | BindingFlags.Instance)
            .Where(mi => mi.MemberType == MemberTypes.Field || mi.MemberType == MemberTypes.Property);
    }

    string ToCamelCase(string s)
    {
        if (string.IsNullOrEmpty(s)) return s;
        if (s.Length < 2) return s.ToLowerInvariant();
        return char.ToLowerInvariant(s[0]) + s.Substring(1);
    }

    string GetTypeName(MemberInfo mi)
    {
        Type t = (mi is PropertyInfo) ? ((PropertyInfo)mi).PropertyType : ((FieldInfo)mi).FieldType;
        return this.GetTypeName(t);
    }

    string GetTypeName(Type t)
    {
        if(t.IsPrimitive)
        {
            if (t == typeof(bool)) return "bool";
            if (t == typeof(char)) return "string";
            return "number";
        }
        if (t == typeof(decimal)) return "number";            
        if (t == typeof(string)) return "string";
        if (t.IsArray)
        {            
            var at = t.GetElementType();
            return this.GetTypeName(at) + "[]";
        }
        if(typeof (System.Collections.IEnumerable).IsAssignableFrom(t)) 
        {
            var collectionType = t.GetGenericArguments()[0]; // all my enumerables are typed, so there is a generic argument
            return GetTypeName(collectionType) + "[]";
        }            
        if (Nullable.GetUnderlyingType(t) != null)
        {
            return this.GetTypeName(Nullable.GetUnderlyingType(t));
        }
        if(t.IsEnum) return "number";
        if(knownTypes.Contains(t)) return t.Name;
        return "any";
    }

    string Enums<T>() // Enums<>, since Enum<> is not allowed.
    {
        Type t = typeof(T);        
        var sb = new StringBuilder();        
        int[] values = (int[])Enum.GetValues(t);
        sb.AppendLine("var " + t.Name + " = {");
        foreach(var val in values) 
        {
            var name = Enum.GetName(typeof(T), val);
            sb.AppendFormat("{0}: {1},\n", name, val);
        }
        sb.AppendLine("}");
        return sb.ToString();
    }
#>

腳本的下一級將從 MVC JsonController 類創建服務接口。

試試 Reinforced.Typings 框架。 似乎它解決了你的問題。

  1. NuGet安裝
  2. 導航到您的 POCO 並在其上方添加[TsInterface]屬性

    using Reinforced.Typings.Attributes; namespace YourNamespace { [TsInterface] public class YourPoco { public int YourNumber { get;set; } public string YourString { get;set; } public List<string> YourArray { get;set; } public Dictionary<int, object> YourDictionary { get;set; } } }
  3. 重建你的項目
  4. %Your_Project_Directory%/Scripts/project.ts文件中找出生成的 TypeScript 代碼,並手動將其添加到項目中

    module YourNamespace { export interface IYourPoco { YourNumber: number; YourString: string; YourArray: string[]; YourDictionary: { [key: int]: any }; } }
  5. 對所有 POCO 執行相同操作,並在其他 TypeScript 代碼中引用project.ts

文檔 wiki中查看更多詳細信息

這是我解決它的方法。 使用屬性聲明您的 C# 類,將生成 .d.ts 文件(使用 T4 轉換)。 nuget 上有一個包,源代碼在 github 上可用 我仍在從事該項目,但支持非常廣泛。

如果您使用的是 Visual Studio,請添加 Typewriter 擴展。

視覺工作室畫廊

網站/文件

更新

在 VS 2015 中安裝Web Essentials后,您可以右鍵單擊類文件,然后從上下文菜單中選擇 > Web Essentials > 創建 Typescript Intellisense 文件。

我創建了一個小實用程序,可以從 C# 類生成 TypeScript 接口。 作為NuGet 包提供。 可以在項目網頁上找到詳細的文檔。

請看看這個圖書館打字機

它不僅可以轉換類、枚舉、接口等,還可以轉換 api 控制器,這簡直太棒了……

此外,它會在您保存 source.cs 文件后立即執行此操作,因此我不必觸發任何外部工具。 Save.cs 你會得到 updated.ts

我有一個使用 T4 模板的小解決方案( 查看源代碼)。

您來自任何 CLR POCO:

public class Parent : Person
{
    public string Name { get; set; }
    public bool? IsGoodParent { get; set; }
    public virtual ICollection<Child> Children { get; set; }
}

到 TypeScript 接口:

///<reference path="Child.d.ts" />
///<reference path="Person.d.ts" />
interface Parent extends Person {
    Name : string;
    IsGoodParent? : bool;
    Children : Child[];
}

如果您需要為每個生成的 TypeScript 類/接口創建一個單獨的文件(即以“每個文件一個類”的方式),您可以嘗試TypeGen 您可以從包管理器控制台使用它來根據您的 C# 類/枚舉生成 TypeScript 文件。 目前支持:

  • 導出枚舉; 將 POCO 導出為 TS 類或接口
  • 遺產
  • 通用類型
  • 集合/嵌套集合類型

加上一些額外的功能。 它也是開源的(您可以在github上查看)。

您可以使用開源項目NSwag :在 GUI 中,您可以從現有的 .NET DLL 中選擇 .NET 類並為其生成 TypeScript 接口。

該項目還提供命令行工具和對 T4 模板的支持,以及為 Web API 控制器生成客戶端代碼......

伙計們看看https://github.com/reinforced/Reinforced.Typings 最近幾天我一直在玩 typelite 和 t4 模板,並以這個項目結束。 它超級簡單,工作起來很有魅力。 只需獲取軟件包,修改配置文件(大約 10 秒)並構建。 一切都自動完成,沒有任何問題。 祝福作者!

T4 模板的壞處在於,一旦您從 VS 構建,掃描的程序集就會被鎖定,您必須重新啟動 VS(這有多愚蠢?)。 T4 工具箱中有一些解決方法 + 一些 VS 清理指令,但這些都不適合我。

你也可以使用這個: https ://github.com/pankleks/TypeScriptBuilder

這個小型庫基於 C# 類型生成 TypeScript 類型定義。 直接在后端 C# 項目中使用它來為前端 TypeScript 項目生成代碼。 您還可以編寫小型控制台應用程序,通過預構建工具生成代碼。

適用於 Full & NET Core 框架!

通過 nuget 安裝: Install-Package TypeScriptBuilder

支持的功能

  • 解決類型依賴
  • 泛型
  • 類型繼承
  • 命名空間(模塊)
  • 枚舉
  • 可空類型
  • 字典轉換(強類型 TS 索引對象)
  • 一組代碼生成控制屬性
  • any對於無法轉換的類型

更多說明: https ://github.com/pankleks/TypeScriptBuilder/blob/master/README.md

我在開發者社區頁面上寫了一個關於這個的功能請求:

https://developercommunity.visualstudio.com/idea/1153873/reuse-existing-net-classes-for-typescript-definiti.html

正如您在此處的答案中看到的那樣,有許多項目試圖解決此問題,但不幸的是,其中許多項目都是單一開發人員項目,在此過程中不再受到支持。 我認為,如果微軟改為維護這些項目之一,創建一個.NETTypeScript生成器,那將是非常好的。

其中,有些仍在維護但嚴重依賴單個開發人員:

類型精簡版:

https://bitbucket.org/LukasKabrt/typelite/src/default/

TypeScript 定義生成器:

https://marketplace.visualstudio.com/items?itemName=MadsKristensen.TypeScriptDefinitionGenerator

打字機:

https://github.com/frhagn/Typewriter

類型代:

https://github.com/jburzynski/TypeGen

強化。打字:

https://github.com/reinforced/Reinforced.Typings

我的建議是選擇一個您從一開始就希望停止維護並准備切換到當前維護的項目的項目。 我會選擇使用注釋的東西,如果該項目不再受支持,我可以用其他東西簡單地替換那個標簽。

反過來呢?

查看erecruit TypeScript Translator 它帶有隨時可用的 C# 支持,但實際上是基於模板的(使用 Nunjucks 進行渲染),這意味着它可以生成任何其他東西 - VB.NET、F#、C++、XML、SQL - 任何你可以用模板編碼的東西。

作為 .NET 控制台程序、NodeJS 程序(對於那些不在 Windows 上的程序)或作為Visual Studio 擴展,完成保存時生成功能。 並包括 MSBuild 支持,只是為了讓您的構建服務器滿意。 :-)

我喜歡 citykid 解決方案。 我把它延長了一點。 因此,解決方案也基於使用 T4 模板的代碼生成技術。

它可以生成常見的 TypeScript 類型和環境聲明。

它支持繼承和接口實現。

支持泛型、數組和列表作為類型字段。

它還轉換為未在配置中明確提及的 TypeScript 類型(例如,我們導入類型 A,並且在 TS 輸出中您可以找到一些其他類型:字段類型、基本類型和接口)。

您還可以覆蓋類型的名稱。

也支持枚舉。

使用示例(您可以在項目存儲庫中找到它):

// set extension of the generated TS file
<#@ output extension=".d.ts" #>

// choose the type of TS import TsMode.Ambient || TsMode.Class
<# var tsBuilder = new TsBuilder(TsMode.Ambient); #>

// reference assembly with the c# types to be transformed
<#@ assembly name="$(SolutionDir)artifacts\...\CsT4Ts.Tests.dll" #>

// reference namespaces
<#@ import namespace="CsT4Ts.Tests" #>
<#
    //add types to processing
    tsBuilder.ConsiderType(typeof(PresetDTOBase), "PresetBase");
    tsBuilder.ConsiderType(typeof(PresetDTO), "Preset");
    tsBuilder.ConsiderType(typeof(TestInterface<,>));
#>

// include file with transformation algorithms
<#@ include file="CsT4Ts.t4" #>

你會得到一個輸出

//CsT4Ts.Tests.PresetDTOBase => PresetBase
// CsT4Ts.Tests.PresetDTO => Preset
// CsT4Ts.Tests.TestInterface`2 => TestInterface
// CsT4Ts.Tests.TestEnum => TestEnum

declare class PresetBase
{
    PresetId: string;
    Title: string;
    InterviewDate: string;
}

declare class Preset extends PresetBase
{
    QuestionsIds: string[];
}

declare interface TestInterface<TA, TB>
{
    A: string;
    B: number;
    C: TestEnum;
    D: TestEnum[];
    E: number[];
    F: TA;
    G: TB[];
}

declare enum TestEnum
{
    Foo = 10,
    Boo = 100
}

在此處查看完整解決方案: https ://bitbucket.org/chandrush/cst4ts

您也可以使用 Bridge.net。 從 1.7 版開始,它支持為 C# 類型生成 TypeScript 定義。 請參閱http://bridge.net/docs/generate-typescript-definitions/

我也非常喜歡@citykid 的回答,所以我將其擴展為一次處理整個命名空間。 只需將 POCO 類放入命名空間,然后重建 T4 模板。 我希望我知道如何為每個文件生成單獨的文件,但這不是世界末日。

您需要引用頂部的 .DLL 文件(您想要的類所在的位置),並且需要提及命名空間。 所有要編輯的行都標有 ACME。 @citykid 的主要榮譽,感激不盡!

<#@ template debug="true" hostSpecific="true" language="C#" #>
<#@ output extension=".ts" #>
<#@ Assembly Name="System.Core.dll" #>
<#@ assembly name="$(TargetDir)YOUR_DLL_NAME_HERE_ACME.dll" #>
<#@ assembly name="$(TargetDir)YOUR_OTHER_DLL_NAME_HERE_ACME.dll" #>
<#@ assembly name="$(TargetDir)YOUR_OTHER_DLL_NAME_HERE_ACME.dll" #>
<#@ assembly name="$(TargetDir)YOUR_OTHER_DLL_NAME_HERE_ACME.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Reflection" #>

<#= Process("My.Very.Special.Namespace.ACME") #>
<#= Process("My.Other.Very.Special.Namespace.ACME") #>
<#= Process("My.Other.Very.Special.Namespace.ACME") #>
<#= Process("My.Other.Very.Special.Namespace.ACME") #>

<#+  

    List<Type> knownTypes = new List<Type>();

    string Process(string nameSpace) {
      var allass = AppDomain.CurrentDomain.GetAssemblies();
      var ss = "";
      foreach (var ass in allass)
      {
         ss += ProcessAssembly(ass, nameSpace);
      }
      return ss;
    }

   string ProcessAssembly(Assembly asm, string nameSpace) {
      try {
            Type[] types;
            try
            {
                types = asm.GetTypes();
            }
            catch (ReflectionTypeLoadException e)
            {
                types = e.Types;
            }
            var s = "";
            foreach (var t in types.Where(t => t != null))
            {
               try {

               if (String.Equals(t.Namespace, nameSpace, StringComparison.Ordinal))
               {
                    s += InterfaceOfType(t);
               }

               } catch (Exception e)
               {
               }
            }
            return s;      
      }
      catch (Exception ee2) {
        return "// ERROR LOADING TYPES: " + ee2;
      }

   }

    string InterfaceOfType(Type T)
    {   
        Type t = T;     
        var sb = new StringBuilder();
        sb.AppendFormat("interface {0} {{\r\n", t.Name);
        foreach (var mi in GetInterfaceMembers(t))
        {
            sb.AppendFormat("  {0}: {1};\r\n", this.ToCamelCase(mi.Name), GetTypeName(mi));
        }
        sb.AppendLine("}");
        knownTypes.Add(t);
        return sb.ToString();
    }

    string Interface<T>()
    {   
        Type t = typeof(T);     
        var sb = new StringBuilder();
        sb.AppendFormat("interface {0} {{\n", t.Name);
        foreach (var mi in GetInterfaceMembers(t))
        {
            sb.AppendFormat("  {0}: {1};\r\n", this.ToCamelCase(mi.Name), GetTypeName(mi));
        }
        sb.AppendLine("}");
        knownTypes.Add(t);
        return sb.ToString();
    }

    IEnumerable<MemberInfo> GetInterfaceMembers(Type type)
    {
        return type.GetMembers(BindingFlags.Public | BindingFlags.Instance)
            .Where(mi => mi.MemberType == MemberTypes.Field || mi.MemberType == MemberTypes.Property);
    }

    string ToCamelCase(string s)
    {
        if (string.IsNullOrEmpty(s)) return s;
        if (s.Length < 2) return s.ToLowerInvariant();
        return char.ToLowerInvariant(s[0]) + s.Substring(1);
    }

    string GetTypeName(MemberInfo mi)
    {
        Type t = (mi is PropertyInfo) ? ((PropertyInfo)mi).PropertyType : ((FieldInfo)mi).FieldType;
        return this.GetTypeName(t);
    }

    string GetTypeName(Type t)
    {
        if(t.IsPrimitive)
        {
            if (t == typeof(bool)) return "boolean";
            if (t == typeof(char)) return "string";
            return "number";
        }
        if (t == typeof(decimal)) return "number";            
        if (t == typeof(string)) return "string";
        if (t.IsArray)
        {            
            var at = t.GetElementType();
            return this.GetTypeName(at) + "[]";
        }
        if(typeof (System.Collections.IEnumerable).IsAssignableFrom(t)) 
        {
            var collectionType = t.GetGenericArguments()[0]; // all my enumerables are typed, so there is a generic argument
            return GetTypeName(collectionType) + "[]";
        }            
        if (Nullable.GetUnderlyingType(t) != null)
        {
            return this.GetTypeName(Nullable.GetUnderlyingType(t));
        }
        if(t.IsEnum) return "number";
        if(knownTypes.Contains(t)) return t.Name;
        return "any";
    }

    string Enums<T>() // Enums<>, since Enum<> is not allowed.
    {
        Type t = typeof(T);        
        var sb = new StringBuilder();        
        int[] values = (int[])Enum.GetValues(t);
        sb.AppendLine("var " + t.Name + " = {");
        foreach(var val in values) 
        {
            var name = Enum.GetName(typeof(T), val);
            sb.AppendFormat("{0}: {1},\r\n", name, val);
        }
        sb.AppendLine("}");
        return sb.ToString();
    }
#>

我的解決方案是編寫一個小型 codegen 實用程序,它只需要一個項目程序集(和引用程序集)並開始掃描涉及 typescript 和 c# 之間交互的類型。 此實用程序將兩個 javascript 都輸出為 d.ts...該工具在構建后事件中被調用...就像一個魅力!

如果有興趣,您可以使用TypedRpc 它的目的不僅是在 TypeScript 中創建接口,而且還使用 JsonRpc 協議在 .Net 中創建與服務的所有通信。

服務器中的類示例:

[TypedRpc.TypedRpcHandler]
public class RpcServerExample
{
    public String HelloWorld()
    {
        return "Hello World!";
    }
}

生成的 TypeScript 代碼的用法:

/// <reference path="Scripts/TypedRpc.ts" />

let rpc: TypedRpc.RpcServerExample = new TypedRpc.RpcServerExample();

var callback = function(data, jsonResponse) {
    console.log(data);
};

rpc.HelloWorld().done(callback).fail(callback);

查看https://github.com/Rodris/TypedRpc了解如何使用它的其他示例。

在 SDLC 期間, ASP.NET Web API 客戶端生成器可能比 swagger 工具鏈和其他工具更方便、開銷更少。

雖然程序員通常使用 WebApiClientGen 生成客戶端 API 代碼,但該項目還提供了 POCO2TS.exe,這是一個從 POCO 類生成 TypsScript 接口的命令行程序。 您可以使用 Poco2ts.exe 或 poco2ts 組件將代碼生成與構建管道集成。

如果你想通過 node.js 轉換它,那么你可以使用這個包(csharp-to-typescript)。 https://www.npmjs.com/package/csharp-to-typescript

源代碼: https ://github.com/YuvrajSagarRana/csharp-to-typescript

eg: 
// After installation, import the package.
var { CsharpToTs, getConfiguration } = require("csharp-to-typescript");

// Use CsharpToTs to convert your source code a. Method one give your source code as string:
const sourceCodeInString =   `public class Address
{
  public int Id {get; set;}
  public string Street { get; set; }
  public string City { get; set; }
}`

var outputTypescript = CsharpToTs(sourceCodeInString, getConfiguration());
console.log(outputTypescript);

// Output is

export class Address
{
  Id: number;
  Street: string;
  City: string;
}

如果您使用的是 VSCode,則可以查看C# to TypeScript的擴展

將 C# 類轉換為 TypeScript 接口

了解如何通過單擊將其從 C# 代碼轉換。 當然,並不是所有的類都會被轉換,比如工廠,但是對於客戶端來說已經足夠好了。 我們只需要數據的形狀。 有時如果我需要使用訪問者模式,我會用我需要的額外方法來裝飾。 這樣做只花了大約 5 秒鍾。 與上面的一分鍾相比,我可以說這是一個巨大的勝利。

在我的博客文章中查看更多詳細信息

我使用生成 TypeScript 和 C# DTO/POCO 類的小型自定義 DSL。 DSL 如下所示:

output CSharp
output TypeScript

namespace MyLibrary.Logic.DataModel.DTOs

class Something

property int ID
property string Name
property DateTime DateTime
property int ForeignKeyID

通過這種方式,我有一個 DTO 模型,它具有單一來源(DSL)並且位於兩個 C# 和 TypeScript 邏輯模型之間。

然后,我編寫了一個 C# 和 TypeScript DTOFactory 類,這些類具有負責在 DTO 和邏輯模型對象之間進行轉換的方法。 (DTO 生成的一部分是序列化方法,這些方法轉換為適合序列化或存儲在 IndexedDB 中的匿名 JSON 類型。)

通過這樣做,一旦任何一方的模型發生變化,我就會得到編譯錯誤,直到雙方再次匹配。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM