Generación de código en Go con el ejemplo de crear un cliente para la base de datos

En este artículo, me gustaría considerar los problemas de generación de código en Golang. Me di cuenta de que a menudo en los comentarios sobre los artículos sobre Go mencionan la generación y la reflexión del código, lo que provoca un acalorado debate. Al mismo tiempo, hay pocos artículos sobre la generación de código en el hub, aunque se usa bastante en proyectos en Go. En el artículo intentaré decirle qué es la generación de código, para describir el alcance de la aplicación con ejemplos de código. Además, no ignoraré la reflexión.

Cuando se usa la generación de código


En Habré ya hay buenos artículos sobre el tema aquí y aquí , no repetiré.

La generación de código debe usarse en casos:

  • Aumentar la velocidad del código, es decir, reemplazar la reflexión;
  • Reducir la rutina de un programador (y los errores asociados con él);
  • Implementación de envoltorios de acuerdo a las reglas dadas.

A partir de los ejemplos, podemos considerar la biblioteca Stringer, que se incluye en el suministro de idioma estándar y le permite generar automáticamente métodos String () para conjuntos de constantes numéricas. Utilizándolo, puede implementar la salida de nombres de variables. Ejemplos de la biblioteca se describieron en detalle en los artículos anteriores. El ejemplo más interesante fue la derivación del nombre del color de la paleta. La aplicación de generación de código allí evita cambiar el código en varios lugares al cambiar la paleta.

De un ejemplo más práctico, podemos mencionar la biblioteca easyjson de Mail.ru. Esta biblioteca le permite acelerar la ejecución de masrshall / unmarshall JSON desde / hacia la estructura. Su implementación en los puntos de referencia omitió todas las alternativas. Para usar la biblioteca, debe llamar a easyjson, generará código para todas las estructuras que encuentra en el archivo transferido, o solo para aquellas a las que se indica el comentario // easyjson: json. Tome la estructura del usuario como ejemplo:

type User struct{
    ID int
    Login string
    Email string
    Level int
}

Para el archivo en el que está contenido, ejecute la generación de código:

easyjson -all main.go

Como resultado, obtenemos métodos para Usuario:

  • MarshalEasyJSON (w * jwriter.Writer): para convertir la estructura en una matriz de bytes JSON;
  • UnmarshalEasyJSON (l * jlexer.Lexer): para convertir una matriz de bytes en una estructura.

Las funciones MarshalJSON () ([] byte, error) y UnmarshalJSON (data [] byte) error son necesarias para la compatibilidad con la interfaz json estándar.

Código Easyjson

func TestEasyJSON() {
	testJSON := `{"ID":123, "Login":"TestUser", "Email":"user@gmail.com", "Level":12}`
	JSONb := []byte(testJSON)
	fmt.Println(testJSON)
	recvUser := &User{}
	recvUser.UnmarshalJSON(JSONb)
	fmt.Println(recvUser)
	recvUser.Level += 1
	outJSON, _ := recvUser.MarshalJSON()
	fmt.Println(string(outJSON))
}


En esta función, primero convertimos JSON en una estructura, agregamos un nivel e imprimimos el JSON resultante. La generación de código por easyjson significa deshacerse de la reflexión en tiempo de ejecución y aumentar el rendimiento del código.

La generación de código se usa activamente para crear microservicios que se comunican a través de gRPC. Utiliza el formato protobuf para describir los métodos de servicios, utilizando el lenguaje intermedio EDL. Después de la descripción del servicio, se inicia el compilador de protocolos, que genera código para el lenguaje de programación deseado. En el código generado, obtenemos las interfaces que deben implementarse en el servidor y los métodos que se utilizan en el cliente para organizar la comunicación. Resulta bastante conveniente, podemos describir nuestros servicios en un solo formato y generar código para el lenguaje de programación en el que se describirá cada uno de los elementos de interacción.

Además, la generación de código se puede utilizar en el desarrollo de marcos. Por ejemplo, para implementar código que no es necesario que el desarrollador de la aplicación escriba, pero es necesario para su correcto funcionamiento. Por ejemplo, para crear validadores de campo de formulario, generación automática de Middleware, generación dinámica de clientes para el DBMS.

Implementación de Go Code Generator


Examinemos en la práctica cómo funciona el mecanismo de generación de código en Go. En primer lugar, es necesario mencionar el AST - Árbol de sintaxis abstracta o Árbol de sintaxis abstracta. Para más detalles, puede ir a Wikipedia . Para nuestros propósitos, es necesario comprender que todo el programa se construye en forma de gráfico, donde los vértices se asignan (marcan) con los operadores del lenguaje de programación y las hojas con los operandos correspondientes.

Entonces, para empezar, necesitamos paquetes:

go / ast
go / parser /
go / token / El

siguiente comando realiza el análisis del archivo con el código y la compilación del árbol.


fset := token.NewFileSet()
node, err := parser.ParseFile(fset, os.Args[1], nil, parser.ParseComments)

Indicamos que el nombre del archivo debe tomarse del primer argumento de la línea de comando, también solicitamos que se agreguen comentarios al árbol.

En general, para controlar la generación de código, el usuario (el desarrollador del código en función del cual se genera otro código) puede usar comentarios o etiquetas (mientras escribimos `json:" "` cerca del campo de estructura).

Por ejemplo, escribiremos un generador de código para trabajar con una base de datos. El generador de código examinará el archivo transferido, buscará estructuras que tengan un comentario correspondiente y creará un contenedor sobre la estructura (métodos CRUD) para la interacción de la base de datos. Utilizaremos los parámetros:

  • comentario de dbe: {"table": "users"}, en el que puede definir la tabla en la que estarán los registros de estructura;
  • Etiqueta dbe para los campos de la estructura, en la que puede especificar el nombre de la columna en la que colocar el valor del campo y los atributos para la base de datos: primary_key y not_null. Se utilizarán al crear la tabla. Y para el nombre del campo, puede usar "-" para no crear una columna para él.

Haré una reserva por adelantado de que el proyecto aún no está en combate, no contendrá parte de las verificaciones y protecciones necesarias. Si hay interés, continuaré su desarrollo.

Entonces, hemos decidido la tarea y los parámetros para controlar la generación de código, podemos comenzar a escribir código.

Los enlaces a todo el código estarán al final del artículo.

Comenzamos a pasar por alto el árbol resultante y analizaremos cada elemento del primer nivel. Go tiene tipos predefinidos para el análisis: BadDecl, GenDecl y FuncDecl.

Descripción del tipo
// A BadDecl node is a placeholder for declarations containing
// syntax errors for which no correct declaration nodes can be
// created.
//
BadDecl struct {
    From, To token.Pos // position range of bad declaration
}
// A GenDecl node (generic declaration node) represents an import,
// constant, type or variable declaration. A valid Lparen position
// (Lparen.IsValid()) indicates a parenthesized declaration.
//
// Relationship between Tok value and Specs element type:
//
// token.IMPORT *ImportSpec
// token.CONST *ValueSpec
// token.TYPE *TypeSpec
// token.VAR *ValueSpec
//
GenDecl struct {
    Doc *CommentGroup // associated documentation; or nil
    TokPos token.Pos // position of Tok
    Tok token.Token // IMPORT, CONST, TYPE, VAR
    Lparen token.Pos // position of '(', if any
    Specs []Spec
    Rparen token.Pos // position of ')', if any
}
// A FuncDecl node represents a function declaration.
FuncDecl struct {
    Doc *CommentGroup // associated documentation; or nil
    Recv *FieldList // receiver (methods); or nil (functions)
    Name *Ident // function/method name
    Type *FuncType // function signature: parameters, results, and position of "func" keyword
    Body *BlockStmt // function body; or nil for external (non-Go) function
}


Estamos interesados ​​en las estructuras, por eso usamos GenDecl. En esta etapa, FuncDecl puede ser útil, en el que se encuentran las definiciones de funciones y usted las ajusta, pero ahora no las necesitamos. A continuación, observamos la matriz de especificaciones en cada nodo y buscamos que estamos trabajando con un campo de definición de tipo (* ast.TypeSpec) y esta es una estructura (* ast.StructType). Después de determinar que tenemos una estructura, verificamos que tenga un comentario // dbe. El código transversal del árbol completo y la definición de con qué estructura trabajar se encuentran a continuación.

Atravesar árboles y obtener estructuras

for _, f := range node.Decls {
	genD, ok := f.(*ast.GenDecl)
	if !ok {
		fmt.Printf("SKIP %T is not *ast.GenDecl\n", f)
		continue
	}
	targetStruct := &StructInfo{}
	var thisIsStruct bool
	for _, spec := range genD.Specs {
		currType, ok := spec.(*ast.TypeSpec)
		if !ok {
			fmt.Printf("SKIP %T is not ast.TypeSpec\n", spec)
			continue
		}

		currStruct, ok := currType.Type.(*ast.StructType)
		if !ok {
			fmt.Printf("SKIP %T is not ast.StructType\n", currStruct)
			continue
		}
		targetStruct.Name = currType.Name.Name
		thisIsStruct = true
	}
	//Getting comments
	var needCodegen bool
	var dbeParams string
	if thisIsStruct {
		for _, comment := range genD.Doc.List {
			needCodegen = needCodegen || strings.HasPrefix(comment.Text, "// dbe")
			if len(comment.Text) < 7 {
				dbeParams = ""
			} else {
				dbeParams = strings.Replace(comment.Text, "// dbe:", "", 1)
			}
		}
	}
	if needCodegen {
		targetStruct.Target = genD
		genParams := &DbeParam{}
		if len(dbeParams) != 0 {
			err := json.Unmarshal([]byte(dbeParams), genParams)
			if err != nil {
				fmt.Printf("Error encoding DBE params for structure %s\n", targetStruct.Name)
				continue
			}
		} else {
			genParams.TableName = targetStruct.Name
		}

		targetStruct.GenParam = genParams
		generateMethods(targetStruct, out)
	}
}

, :

type DbeParam struct {
	TableName string `json:"table"`
}

type StructInfo struct {
	Name     string
	GenParam *DbeParam
	Target   *ast.GenDecl
}


Ahora prepararemos información sobre los campos de la estructura, de modo que, en función de la información recibida, generaremos funciones de creación de tablas (createTable) y métodos CRUD.

Código para obtener campos de una estructura

func generateMethods(reqStruct *StructInfo, out *os.File) {
	for _, spec := range reqStruct.Target.Specs {
		fmt.Fprintln(out, "")
		currType, ok := spec.(*ast.TypeSpec)
		if !ok {
			continue
		}
		currStruct, ok := currType.Type.(*ast.StructType)
		if !ok {
			continue
		}

		fmt.Printf("\tgenerating createTable methods for %s\n", currType.Name.Name)

		curTable := &TableInfo{
			TableName: reqStruct.GenParam.TableName,
			Columns:   make([]*ColInfo, 0, len(currStruct.Fields.List)),
		}

		for _, field := range currStruct.Fields.List {
			if len(field.Names) == 0 {
				continue
			}
			tableCol := &ColInfo{FieldName: field.Names[0].Name}
			var fieldIsPrimKey bool
			var preventThisField bool
			if field.Tag != nil {
				tag := reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1])
				tagVal := tag.Get("dbe")
				fmt.Println("dbe:", tagVal)
				tagParams := strings.Split(tagVal, ",")
			PARAMSLOOP:
				for _, param := range tagParams {
					switch param {
					case "primary_key":
						if curTable.PrimaryKey == nil {
							fieldIsPrimKey = true
							tableCol.NotNull = true
						} else {
							log.Panicf("Table %s cannot have more then 1 primary key!", currType.Name.Name)
						}
					case "not_null":
						tableCol.NotNull = true
					case "-":
						preventThisField = true
						break PARAMSLOOP
					default:
						tableCol.ColName = param
					}

				}
				if preventThisField {
					continue
				}
			}
			if tableCol.ColName == "" {
				tableCol.ColName = tableCol.FieldName
			}
			if fieldIsPrimKey {
				curTable.PrimaryKey = tableCol
			}
			//Determine field type
			var fieldType string
			switch field.Type.(type) {
			case *ast.Ident:
				fieldType = field.Type.(*ast.Ident).Name
			case *ast.SelectorExpr:
				fieldType = field.Type.(*ast.SelectorExpr).Sel.Name
			}
			//fieldType := field.Type.(*ast.Ident).Name
			fmt.Printf("%s- %s\n", tableCol.FieldName, fieldType)
			//Check for integers
			if strings.Contains(fieldType, "int") {
				tableCol.ColType = "integer"
			} else {
				//Check for other types
				switch fieldType {
				case "string":
					tableCol.ColType = "text"
				case "bool":
					tableCol.ColType = "boolean"
				case "Time":
					tableCol.ColType = "TIMESTAMP"
				default:
					log.Panicf("Field type %s not supported", fieldType)
				}
			}
			tableCol.FieldType = fieldType
			curTable.Columns = append(curTable.Columns, tableCol)
			curTable.StructName = currType.Name.Name

		}
		curTable.generateCreateTable(out)

		fmt.Printf("\tgenerating CRUD methods for %s\n", currType.Name.Name)
		curTable.generateCreate(out)
		curTable.generateQuery(out)
		curTable.generateUpdate(out)
		curTable.generateDelete(out)
	}
}


Revisamos todos los campos de la estructura deseada y comenzamos a analizar las etiquetas de cada campo. Usando la reflexión, obtenemos la etiqueta que nos interesa (después de todo, puede haber otras etiquetas en el campo, por ejemplo, para json). Analizamos el contenido de la etiqueta y determinamos si el campo es una clave primaria (si se especifica más de una clave primaria, la maldecimos y detenemos la ejecución), ¿hay un requisito para que el campo sea distinto de cero? ¿Necesitamos trabajar con la base de datos para este campo y definir nombre de columna si se anuló en la etiqueta. También necesitamos determinar el tipo de columna de la tabla en función del tipo de campo de estructura. Hay un conjunto finito de tipos de campo, generaremos solo para los tipos básicos, reduciremos todas las filas al tipo de campo TEXTO, aunque en general, puede agregar la definición del tipo de columna a las etiquetas para que pueda configurar más finamente. Por otra parte,nadie se molesta en crear la tabla deseada en la base de datos por adelantado, o en corregir la creada automáticamente.

Después de analizar la estructura, comenzamos el método para crear el código para la función de creación de tablas y los métodos para crear las funciones Crear, Consultar, Actualizar, Eliminar. Preparamos una expresión SQL para cada función y un enlace para ejecutar. No me molesté en el manejo de errores, solo di el error del controlador de la base de datos. Para la generación de código, es conveniente usar plantillas de la biblioteca de texto / plantilla. Con su ayuda, puede obtener un código mucho más compatible y predecible (el código es visible de inmediato, pero no está manchado por el código del generador).

Creación de tabla

func (tableD *TableInfo) generateCreateTable(out *os.File) error {
	fmt.Fprint(out, "func (in *"+tableD.StructName+") createTable(db *sql.DB) (error) {\n")
	var resSQLq = fmt.Sprintf("\tsqlQ := `CREATE TABLE %s (\n", tableD.TableName)
	for _, col := range tableD.Columns {
		colSQL := col.ColName + " " + col.ColType
		if col.NotNull {
			colSQL += " NOT NULL"
		}
		if col == tableD.PrimaryKey {
			colSQL += " AUTO_INCREMENT"
		}
		colSQL += ",\n"
		resSQLq += colSQL
	}
	if tableD.PrimaryKey != nil {
		resSQLq += fmt.Sprintf("PRIMARY KEY (%s)\n", tableD.PrimaryKey.ColName)
	}
	resSQLq += ")`\n"
	fmt.Fprint(out, resSQLq)
	fmt.Fprint(out, "\t_, err := db.Exec(sqlQ)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n")
	fmt.Fprint(out, "\t return nil\n}\n\n")
	return nil
}


Agregar registro


	fmt.Fprint(out, "func (in *"+tableD.StructName+") Create(db *sql.DB) (error) {\n")
	var columns, valuePlaces, valuesListParams string
	for _, col := range tableD.Columns {
		if col == tableD.PrimaryKey {
			continue
		}
		columns += "`" + col.ColName + "`,"
		valuePlaces += "?,"
		valuesListParams += "in." + col.FieldName + ","
	}
	columns = columns[:len(columns)-1]
	valuePlaces = valuePlaces[:len(valuePlaces)-1]
	valuesListParams = valuesListParams[:len(valuesListParams)-1]

	resSQLq := fmt.Sprintf("\tsqlQ := \"INSERT INTO %s (%s) VALUES (%s);\"\n",
		tableD.TableName,
		columns,
		valuePlaces)
	fmt.Fprintln(out, resSQLq)
	fmt.Fprintf(out, "result, err := db.Exec(sqlQ, %s)\n", valuesListParams)
	fmt.Fprintln(out, `if err != nil {
		return err
	}`)
	//Setting id if we have primary key
	if tableD.PrimaryKey != nil {
		fmt.Fprintf(out, `lastId, err := result.LastInsertId()
		if err != nil {
			return nil
		}`)
		fmt.Fprintf(out, "\nin.%s = %s(lastId)\n", tableD.PrimaryKey.FieldName, tableD.PrimaryKey.FieldType)
	}
	fmt.Fprintln(out, "return nil\n}\n\n")
	//in., _ := result.LastInsertId()`)
	return nil
}


Recuperando registros de una tabla

func (tableD *TableInfo) generateQuery(out *os.File) error {
	fmt.Fprint(out, "func (in *"+tableD.StructName+") Query(db *sql.DB) ([]*"+tableD.StructName+", error) {\n")

	fmt.Fprintf(out, "\tsqlQ := \"SELECT * FROM %s;\"\n", tableD.TableName)
	fmt.Fprintf(out, "rows, err := db.Query(sqlQ)\n")
	fmt.Fprintf(out, "results := make([]*%s, 0)\n", tableD.StructName)
	fmt.Fprintf(out, `for rows.Next() {`)
	fmt.Fprintf(out, "\t tempR := &%s{}\n", tableD.StructName)
	var valuesListParams string
	for _, col := range tableD.Columns {
		valuesListParams += "&tempR." + col.FieldName + ","
	}
	valuesListParams = valuesListParams[:len(valuesListParams)-1]

	fmt.Fprintf(out, "\terr = rows.Scan(%s)\n", valuesListParams)
	fmt.Fprintf(out, `if err != nil {
		return nil, err
		}`)
	fmt.Fprintf(out, "\n\tresults = append(results, tempR)")
	fmt.Fprintf(out, `}
		return results, nil
	}`)
	fmt.Fprintln(out, "")
	fmt.Fprintln(out, "")
	return nil
}


Actualización de registro (funciona por clave principal)

func (tableD *TableInfo) generateUpdate(out *os.File) error {
	fmt.Fprint(out, "func (in *"+tableD.StructName+") Update(db *sql.DB) (error) {\n")
	var updVals, valuesListParams string
	for _, col := range tableD.Columns {
		if col == tableD.PrimaryKey {
			continue
		}
		updVals += "`" + col.ColName + "`=?,"
		valuesListParams += "in." + col.FieldName + ","
	}
	updVals = updVals[:len(updVals)-1]
	valuesListParams += "in." + tableD.PrimaryKey.FieldName

	resSQLq := fmt.Sprintf("\tsqlQ := \"UPDATE %s SET %s WHERE %s = ?;\"\n",
		tableD.TableName,
		updVals,
		tableD.PrimaryKey.ColName)
	fmt.Fprintln(out, resSQLq)
	fmt.Fprintf(out, "_, err := db.Exec(sqlQ, %s)\n", valuesListParams)
	fmt.Fprintln(out, `if err != nil {
		return err
	}`)

	fmt.Fprintln(out, "return nil\n}\n\n")
	//in., _ := result.LastInsertId()`)
	return nil
}


Eliminar un registro (funciona por clave primaria)

func (tableD *TableInfo) generateDelete(out *os.File) error {
	fmt.Fprint(out, "func (in *"+tableD.StructName+") Delete(db *sql.DB) (error) {\n")
	fmt.Fprintf(out, "sqlQ := \"DELETE FROM %s WHERE id = ?\"\n", tableD.TableName)

	fmt.Fprintf(out, "_, err := db.Exec(sqlQ, in.%s)\n", tableD.PrimaryKey.FieldName)

	fmt.Fprintln(out, `if err != nil {
		return err
	}
	return nil
}`)
	fmt.Fprintln(out)
	return nil
}


El inicio del generador de código resultante se realiza mediante la ejecución habitual de ejecución, pasamos la ruta al archivo para el que desea generar el código en el indicador -name. Como resultado, obtenemos el archivo con el sufijo _dbe, en el que se encuentra el código generado. Para las pruebas, cree métodos para la siguiente estructura:


// dbe:{"table": "users"}
type User struct {
	ID       int    `dbe:"id,primary_key"`
	Login    string `dbe:"login,not_null"`
	Email    string
	Level    uint8
	IsActive bool
	UError   error `dbe:"-"`
}

El código resultante

package main

import "database/sql"

func (in *User) createTable(db *sql.DB) error {
	sqlQ := `CREATE TABLE users (
	id integer NOT NULL AUTO_INCREMENT,
	login text NOT NULL,
	Email text,
	Level integer,
	IsActive boolean,
	PRIMARY KEY (id)
	)`
	_, err := db.Exec(sqlQ)
	if err != nil {
		return err
	}
	return nil
}

func (in *User) Create(db *sql.DB) error {
	sqlQ := "INSERT INTO users (`login`,`Email`,`Level`,`IsActive`) VALUES (?,?,?,?);"

	result, err := db.Exec(sqlQ, in.Login, in.Email, in.Level, in.IsActive)
	if err != nil {
		return err
	}
	lastId, err := result.LastInsertId()
	if err != nil {
		return nil
	}
	in.ID = int(lastId)
	return nil
}

func (in *User) Query(db *sql.DB) ([]*User, error) {
	sqlQ := "SELECT * FROM users;"
	rows, err := db.Query(sqlQ)
	results := make([]*User, 0)
	for rows.Next() {
		tempR := &User{}
		err = rows.Scan(&tempR.ID, &tempR.Login, &tempR.Email, &tempR.Level, &tempR.IsActive)
		if err != nil {
			return nil, err
		}
		results = append(results, tempR)
	}
	return results, nil
}

func (in *User) Update(db *sql.DB) error {
	sqlQ := "UPDATE users SET `login`=?,`Email`=?,`Level`=?,`IsActive`=? WHERE id = ?;"

	_, err := db.Exec(sqlQ, in.Login, in.Email, in.Level, in.IsActive, in.ID)
	if err != nil {
		return err
	}
	return nil
}

func (in *User) Delete(db *sql.DB) error {
	sqlQ := "DELETE FROM users WHERE id = ?"
	_, err := db.Exec(sqlQ, in.ID)
	if err != nil {
		return err
	}
	return nil
}


Para probar el funcionamiento del código generado, cree un objeto con datos arbitrarios, cree una tabla para él (si la tabla existe en la base de datos, se devolverá un error). Después de colocar este objeto en la tabla, lea todos los campos de la tabla, actualice los valores de nivel y elimine el objeto.

Llama a los métodos resultantes

var err error
db, err := sql.Open("mysql", DSN)
if err != nil {
	fmt.Println("Unable to connect to DB", err)
	return
}
err = db.Ping()
if err != nil {
	fmt.Println("Unable to ping BD")
	return
}
newUser := &User{
	Login:    "newUser",
	Email:    "new@test.com",
	Level:    0,
	IsActive: false,
	UError:   nil,
}

err = newUser.createTable(db)
if err != nil {
	fmt.Println("Error creating table.", err)

}
err = newUser.Create(db)
if err != nil {
	fmt.Println("Error creating user.", err)
	return
}

nU := &User{}
dbUsers, err := nU.Query(db)
if err != nil {
	fmt.Println("Error selecting users.", err)
	return
}
fmt.Printf("From table users selected %d fields", len(dbUsers))
var DBUser *User
for _, user := range dbUsers {
	fmt.Println(user)
	DBUser = user
}
DBUser.Level = 2
err = DBUser.Update(db)
if err != nil {
	fmt.Println("Error updating users.", err)
	return
}
err = DBUser.Delete(db)
if err != nil {
	fmt.Println("Error deleting users.", err)
	return
}


En la implementación actual, la funcionalidad del cliente para la base de datos es muy limitada:

  • solo se admite MySQL;
  • No todos los tipos de campo son compatibles.
  • no hay filtros y límites para SELECT.

Sin embargo, corregir errores ya está fuera del alcance de analizar el código fuente de Go y generar un nuevo código basado en él.

El uso de un generador de código en tal escenario le permitirá cambiar los campos y tipos de estructuras utilizados en la aplicación en un solo lugar, no es necesario recordar hacer cambios en el código para interactuar con la base de datos, solo necesita ejecutar el generador de código cada vez. Esta tarea podría resolverse con la ayuda de la reflexión, pero esto habría afectado el rendimiento.

El generador de código fuente y un ejemplo del código generado publicado en Github .

All Articles