Los UDT (User Defined Types) son tipos personalizados en Oracle:
- Objetos (
SZTYOTIOL_OBJ) - Similar a una clase - Arreglos (
SZTYOTIOL_ARR) - Colección de objetos
-- Tipo Objeto
CREATE TYPE SZTYOTIOL_OBJ AS OBJECT (
SZTYOTIOL_TERM_CODE VARCHAR2(6),
SZTYOTIOL_PIDM NUMBER(8)
);
-- Tipo Arreglo
CREATE TYPE SZTYOTIOL_ARR AS TABLE OF SZTYOTIOL_OBJ;✅ Cuándo usar: Mayoría de casos, tipos simples con muchas propiedades
// ActualizarTiraAutomatizadaRequestDto.cs
public class ActualizarTiraAutomatizadaRequestDto
{
// ✅ Hereda de OracleUdtBase = Mapeo automático por reflexión
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObj : OracleUdtBase
{
[OracleObjectMapping("SZTYOTIOL_TERM_CODE")]
public string? TermCode { get; set; }
[OracleObjectMapping("SZTYOTIOL_PIDM")]
public int? Pidm { get; set; }
// ✨ No necesitas escribir FromCustomObject/ToCustomObject
// ✨ La clase base lo hace automáticamente con reflexión
public static TiraOracleObj Null => CreateNull<TiraOracleObj>();
}
public string TermCode { get; set; }
public TiraOracleObj? Tira { get; set; }
public TiraOracleObj[]? TirasActivas { get; set; }
}
// Factories genéricas (al final del archivo) - Solo 2 líneas!
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObjFactory : OracleUdtFactory<ActualizarTiraAutomatizadaRequestDto.TiraOracleObj> { }
[OracleCustomTypeMapping("SZTYOTIOL_ARR")]
public class TiraOracleArrayFactory : OracleUdtArrayFactory<ActualizarTiraAutomatizadaRequestDto.TiraOracleObj> { }💡 Ventajas:
- ✅ Código ultra compacto (solo declaras propiedades)
- ✅ Perfecto para 10+ propiedades
- ✅ Reflexión automática lee
[OracleObjectMapping] - ✅ No repites código de serialización
- ✅ Factories genéricas reutilizables (2 líneas vs 15)
✅ Cuándo usar: Control total, transformaciones complejas, performance crítico
public class ActualizarTiraAutomatizadaRequestDto
{
// ✅ Implementa IOracleCustomType directamente = Control manual
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObj : IOracleCustomType, INullable
{
private bool _isNull;
[OracleObjectMapping("SZTYOTIOL_TERM_CODE")]
public string? TermCode { get; set; }
[OracleObjectMapping("SZTYOTIOL_PIDM")]
public int? Pidm { get; set; }
public bool IsNull => _isNull;
public static TiraOracleObj Null => new TiraOracleObj { _isNull = true };
// ✅ Control manual: escribes toda la lógica
public void FromCustomObject(OracleConnection con, object pUdt)
{
if (!IsNull)
{
// Transformación personalizada
OracleUdt.SetValue(con, pUdt, "SZTYOTIOL_TERM_CODE",
TermCode?.ToUpper() ?? "");
OracleUdt.SetValue(con, pUdt, "SZTYOTIOL_PIDM", Pidm ?? 0);
}
}
public void ToCustomObject(OracleConnection con, object pUdt)
{
TermCode = (string)OracleUdt.GetValue(con, pUdt, "SZTYOTIOL_TERM_CODE");
Pidm = Convert.ToInt32(OracleUdt.GetValue(con, pUdt, "SZTYOTIOL_PIDM"));
}
}
}
// Factories (igual que Opción 1)💡 Ventajas:
- ✅ Control total sobre serialización
- ✅ Puedes hacer validaciones custom
- ✅ Transformaciones complejas (mayúsculas, formatos, etc.)
- ✅ Sin overhead de reflexión (marginal)
- ❌ Más código para mantener
- ❌ 40 propiedades = 80 líneas manuales
✅ Cuándo usar: Mayoría de propiedades automáticas, algunas manuales
public class ActualizarTiraAutomatizadaRequestDto
{
// ✅ Hereda de base + sobrescribe métodos específicos
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObj : OracleUdtBase
{
[OracleObjectMapping("SZTYOTIOL_TERM_CODE")]
public string? TermCode { get; set; }
[OracleObjectMapping("SZTYOTIOL_PIDM")]
public int? Pidm { get; set; }
// Propiedad compleja que necesita lógica especial
public DateTime? FechaCreacion { get; set; }
// ✅ Sobrescribe solo FromCustomObject para lógica custom
public override void FromCustomObject(OracleConnection con, object pUdt)
{
if (!IsNull)
{
// Llama a la base para propiedades automáticas
base.FromCustomObject(con, pUdt);
// Lógica manual para campos especiales
if (FechaCreacion.HasValue)
{
OracleUdt.SetValue(con, pUdt, "SZTYOTIOL_FECHA",
FechaCreacion.Value.ToString("yyyy-MM-dd"));
}
}
}
public static TiraOracleObj Null => CreateNull<TiraOracleObj>();
}
}💡 Ventajas:
- ✅ Lo mejor de ambos mundos
- ✅ Automático para 95% de propiedades
- ✅ Manual solo donde lo necesitas
- ✅ Código limpio y flexible
// ActualizarTiraAutomatizadaRequestDto.cs
public class ActualizarTiraAutomatizadaRequestDto
{
// ✨ Clase anidada que sirve para AMBOS propósitos:
// 1. DTO para recibir JSON en el API
// 2. UDT para pasar a Oracle
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObj : IOracleCustomType, INullable
{
[OracleObjectMapping("SZTYOTIOL_TERM_CODE")]
public string? TermCode { get; set; }
[OracleObjectMapping("SZTYOTIOL_PIDM")]
public int? Pidm { get; set; }
// === Oracle UDT Implementation ===
private bool _isNull;
public bool IsNull => _isNull;
public void FromCustomObject(OracleConnection con, object pUdt)
{
if (!IsNull)
{
OracleUdt.SetValue(con, pUdt, "SZTYOTIOL_TERM_CODE", TermCode ?? "");
OracleUdt.SetValue(con, pUdt, "SZTYOTIOL_PIDM", Pidm ?? 0);
}
}
public void ToCustomObject(OracleConnection con, object pUdt) { }
}
// === Propiedades del Request ===
public string TermCode { get; set; }
public bool DesmarcarTodos { get; set; }
public TiraOracleObj? Tira { get; set; } // ✅ Objeto
public TiraOracleObj[]? TirasActivas { get; set; } // ✅ Array
}
// === Factories genéricas (al final del mismo archivo) ===
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObjFactory : OracleUdtFactory<ActualizarTiraAutomatizadaRequestDto.TiraOracleObj> { }
[OracleCustomTypeMapping("SZTYOTIOL_ARR")]
public class TiraOracleArrayFactory : OracleUdtArrayFactory<ActualizarTiraAutomatizadaRequestDto.TiraOracleObj> { }using Oracle.ManagedDataAccess.Client;
using Oracle.ManagedDataAccess.Types;
namespace Lince.SERVACAD.Core.Models.Banner;
/// <summary>
/// Clase base para Oracle UDT con serialización automática por reflexión
/// </summary>
public abstract class OracleUdtBase : IOracleCustomType, INullable
{
private bool _isNull;
public bool IsNull => _isNull;
// ✅ Mapeo automático de todas las propiedades con [OracleObjectMapping]
public virtual void FromCustomObject(OracleConnection con, object pUdt)
{
if (!IsNull)
{
foreach (var prop in GetType().GetProperties())
{
var attrData = prop.GetCustomAttributesData()
.FirstOrDefault(a => a.AttributeType == typeof(OracleObjectMappingAttribute));
if (attrData?.ConstructorArguments.Count > 0)
{
string oracleName = attrData.ConstructorArguments[0].Value?.ToString() ?? "";
var value = prop.GetValue(this) ?? GetDefaultValue(prop.PropertyType);
OracleUdt.SetValue(con, pUdt, oracleName, value);
}
}
}
}
public virtual void ToCustomObject(OracleConnection con, object pUdt)
{
foreach (var prop in GetType().GetProperties())
{
var attrData = prop.GetCustomAttributesData()
.FirstOrDefault(a => a.AttributeType == typeof(OracleObjectMappingAttribute));
if (attrData?.ConstructorArguments.Count > 0)
{
string oracleName = attrData.ConstructorArguments[0].Value?.ToString() ?? "";
var value = OracleUdt.GetValue(con, pUdt, oracleName);
prop.SetValue(this, value);
}
}
}
protected virtual object GetDefaultValue(Type propertyType)
{
return propertyType == typeof(string) ? string.Empty :
propertyType == typeof(int?) ? 0 :
propertyType == typeof(decimal?) ? 0m :
propertyType == typeof(DateTime?) ? DateTime.MinValue : 0;
}
protected static T CreateNull<T>() where T : OracleUdtBase, new()
{
return new T { _isNull = true };
}
}
/// <summary>
/// Factory genérica para crear instancias de Oracle UDT
/// </summary>
public class OracleUdtFactory<T> : IOracleCustomTypeFactory where T : IOracleCustomType, new()
{
public IOracleCustomType CreateObject() => new T();
}
/// <summary>
/// Factory genérica para crear arreglos de Oracle UDT
/// </summary>
public class OracleUdtArrayFactory<T> : IOracleArrayTypeFactory where T : IOracleCustomType, new()
{
public Array CreateArray(int numElems) => new T[numElems];
public Array? CreateStatusArray(int numElems) => null;
}using Lince.SERVACAD.Core.Models.Banner;
public class ActualizarTiraAutomatizadaRequestDto
{
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObj : OracleUdtBase
{
[OracleObjectMapping("SZTYOTIOL_TERM_CODE")]
public string? TermCode { get; set; }
[OracleObjectMapping("SZTYOTIOL_PIDM")]
public int? Pidm { get; set; }
public static TiraOracleObj Null => CreateNull<TiraOracleObj>();
}
public string TermCode { get; set; }
public TiraOracleObj? Tira { get; set; }
public TiraOracleObj[]? TirasActivas { get; set; }
}
// Factories genéricas (heredan de OracleUdtBase)
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObjFactory : OracleUdtFactory<ActualizarTiraAutomatizadaRequestDto.TiraOracleObj> { }
[OracleCustomTypeMapping("SZTYOTIOL_ARR")]
public class TiraOracleArrayFactory : OracleUdtArrayFactory<ActualizarTiraAutomatizadaRequestDto.TiraOracleObj> { }public ActualizarTiraAutomatizadaResponseDto ActualizarTirasAutomatizadas(
string termCode,
int? pidm,
string allInd,
ActualizarTiraAutomatizadaRequestDto.TiraOracleObj? tira, // ✅ DTO
ActualizarTiraAutomatizadaRequestDto.TiraOracleObj[]? tirasActivas) // ✅ DTO
{
DataToProcedureBanner procedure = new()
{
PackageName = "SZPKEPTMS.PR_ACTUALIZAR_TIRA_AUTO",
Parameters = new OracleParameter[]
{
new("PIC_TERM_CODE", OracleDbType.Varchar2, termCode, ParameterDirection.Input) { Size = 6 },
new("PIN_PIDM", OracleDbType.Int32, pidm ?? (object)DBNull.Value, ParameterDirection.Input),
new("PIC_ALL_IND", OracleDbType.Varchar2, allInd, ParameterDirection.Input) { Size = 1 },
// ✅ Pasar directo - TiraOracleObj implementa IOracleCustomType
new() {
ParameterName = "PIA_TIRA",
Direction = ParameterDirection.Input,
OracleDbType = OracleDbType.Object, // ← Objeto individual
UdtTypeName = "SZTYOTIOL_OBJ", // ← Case-sensitive
Value = tira ?? (object)DBNull.Value
},
new() {
ParameterName = "PIA_TIRAS_ACT",
Direction = ParameterDirection.Input,
OracleDbType = OracleDbType.Array, // ← Array
UdtTypeName = "SZTYOTIOL_ARR", // ← Case-sensitive
Value = tirasActivas ?? (object)DBNull.Value
},
new("PON_ERROR_CODE", OracleDbType.Int32, ParameterDirection.Output) { Size = 100 },
new("POC_ERROR_DESC", OracleDbType.Varchar2, ParameterDirection.Output) { Size = 3500 }
}
};
return _bannerClient.Execute<ActualizarTiraAutomatizadaResponseDto>(procedure);
}public ActualizarTiraAutomatizadaResponseDto ActualizarTirasAutomatizadas(
ActualizarTiraAutomatizadaRequestDto request)
{
string allInd = request.DesmarcarTodos ? "S" : "N";
// ✅ Pasar directo - Sin conversiones
return _service.ActualizarTirasAutomatizadas(
request.TermCode,
request.Pidm,
allInd,
request.Tira, // ✅ Directo
request.TirasActivas // ✅ Directo
);
}POST /api/ConfigTiraAutomatizada/ActualizarTirasAutomatizadas
{
"termCode": "202510",
"pidm": 123456,
"desmarcarTodos": false,
"tira": {
"termCode": "202510",
"pidm": 123456
},
"tirasActivas": [
{ "termCode": "202510", "pidm": 111111 },
{ "termCode": "202510", "pidm": 222222 }
]
}1. API recibe JSON
↓
2. Se deserializa a ActualizarTiraAutomatizadaRequestDto
↓
3. Controller → Manager → Service (sin conversiones)
↓
4. Service crea OracleParameter con Value = tira
↓
5. Oracle llama automáticamente a tira.FromCustomObject()
↓
6. Datos serializados a Oracle PL/SQL
// ✅ CORRECTO
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")] // Exactamente como en Oracle
[OracleObjectMapping("SZTYOTIOL_TERM_CODE")] // Exactamente como en Oracle
// ❌ INCORRECTO
[OracleCustomTypeMapping("sztyotiol_obj")] // Oracle no lo encontrará// Objeto individual
OracleDbType = OracleDbType.Object,
UdtTypeName = "SZTYOTIOL_OBJ"
// Array de objetos
OracleDbType = OracleDbType.Array,
UdtTypeName = "SZTYOTIOL_ARR"// ✅ Usa factories genéricas reutilizables de OracleUdtBase
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObjFactory : OracleUdtFactory<TiraOracleObj> { }
[OracleCustomTypeMapping("SZTYOTIOL_ARR")]
public class TiraOracleArrayFactory : OracleUdtArrayFactory<TiraOracleObj> { }
// ✅ Solo 2 líneas por tipo! (84% menos código que implementar interfaces)-- Ver tipos UDT
SELECT type_name, typecode
FROM user_types
WHERE typecode IN ('OBJECT', 'COLLECTION')
ORDER BY type_name;
-- Ver estructura de un tipo
DESC SZTYOTIOL_OBJ;
-- Ver atributos exactos (case-sensitive)
SELECT attr_name, attr_type_name
FROM user_type_attrs
WHERE type_name = 'SZTYOTIOL_OBJ'
ORDER BY attr_no;| Error | Causa | Solución |
|---|---|---|
ORA-50028: Invalid parameter binding |
UdtTypeName incorrecto | Verificar mayúsculas exactas |
Type mismatch |
OracleDbType.Object/Array equivocado | Object para objetos, Array para arrays |
Schema mismatch |
Atributos no coinciden | Verificar nombres con user_type_attrs |
PLS-00306: número o tipos de argumentos erróneos |
OracleDbType incorrecto (Int32 vs Decimal) | NUMBER → OracleDbType.Int32 o Decimal |
| Aspecto | Beneficio |
|---|---|
| Un solo archivo | Todo junto, fácil de mantener |
| Cero conversiones | DTO = UDT, pasa directo a Oracle |
| Menos clases | Solo TiraOracleObj (no necesitas TiraOracleUdt) |
| Código limpio | Manager y Service súper simples |
| Type-safe | IntelliSense completo en todo el flujo |
Las factories genéricas están definidas en OracleUdtBase.cs y eliminan la necesidad de escribir código repetitivo.
Antes (Opción Manual - 15 líneas):
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObjFactory : IOracleCustomTypeFactory
{
public IOracleCustomType CreateObject()
{
return new ActualizarTiraAutomatizadaRequestDto.TiraOracleObj();
}
}
[OracleCustomTypeMapping("SZTYOTIOL_ARR")]
public class TiraOracleArrayFactory : IOracleArrayTypeFactory
{
public Array CreateArray(int numElems)
{
return new ActualizarTiraAutomatizadaRequestDto.TiraOracleObj[numElems];
}
public Array? CreateStatusArray(int numElems) => null;
}Ahora (Con Genéricas - 4 líneas):
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObjFactory : OracleUdtFactory<ActualizarTiraAutomatizadaRequestDto.TiraOracleObj> { }
[OracleCustomTypeMapping("SZTYOTIOL_ARR")]
public class TiraOracleArrayFactory : OracleUdtArrayFactory<ActualizarTiraAutomatizadaRequestDto.TiraOracleObj> { }| Aspecto | Manual | Genéricas |
|---|---|---|
| Líneas de código | 15 | 4 |
| Código repetitivo | Alto | Cero |
| Reutilizable | No | Sí |
| Mantenimiento | Difícil | Fácil |
| Type-safe | ✅ | ✅ |
public class OracleUdtFactory<T> : IOracleCustomTypeFactory
where T : IOracleCustomType, new()
{
public IOracleCustomType CreateObject() => new T();
}
public class OracleUdtArrayFactory<T> : IOracleArrayTypeFactory
where T : IOracleCustomType, new()
{
public Array CreateArray(int numElems) => new T[numElems];
public Array? CreateStatusArray(int numElems) => null;
}Simplemente hereda de las factories genéricas:
// Para cualquier nuevo UDT, solo necesitas 2 líneas:
[OracleCustomTypeMapping("MI_NUEVO_TIPO_OBJ")]
public class MiNuevoTipoFactory : OracleUdtFactory<MiClaseUdt> { }
[OracleCustomTypeMapping("MI_NUEVO_TIPO_ARR")]
public class MiNuevoTipoArrayFactory : OracleUdtArrayFactory<MiClaseUdt> { }🎯 Resultado: Creas 10 UDTs diferentes = Solo 40 líneas de factories (vs 150 líneas manual)
Cuando necesites agregar un nuevo tipo UDT:
- Crear clase anidada con
[OracleCustomTypeMapping("TIPO_OBJ")] - Heredar de
OracleUdtBase(Opción 1) o implementarIOracleCustomType(Opción 2) - Agregar
[OracleObjectMapping]a cada propiedad - Si es manual: Implementar
FromCustomObject()yToCustomObject() - Crear factories genéricas heredando de
OracleUdtFactory<T>(2 líneas) - Crear factory de array heredando de
OracleUdtArrayFactory<T>(2 líneas) - Verificar nombres case-sensitive en Oracle (
DESC TIPO_OBJ) - Usar
OracleDbType.Objectpara objetos,.Arraypara arrays - Probar con datos reales
| Criterio | Opción 1: Base Auto | Opción 2: Manual | Opción 3: Híbrido |
|---|---|---|---|
| Líneas de código | 🟢 Mínimo (10-15) | 🔴 Máximo (40-80) | 🟡 Medio (20-30) |
| Propiedades | 🟢 40+ (reflexión) | 🔴 Hasta 10 | 🟢 Cualquier cantidad |
| Control manual | 🔴 No | 🟢 Total | 🟢 Parcial |
| Performance | 🟡 Reflexión (~1ms) | 🟢 Directo | 🟡 Reflexión (~1ms) |
| Mantenimiento | 🟢 Fácil | 🔴 Difícil | 🟢 Fácil |
| Transformaciones | 🔴 No | 🟢 Sí | 🟢 Sí |
| Validaciones custom | 🔴 No | 🟢 Sí | 🟢 Sí |
| Recomendado para | Tipos simples | Control total | Casos especiales |
┌─ ¿Tienes 10+ propiedades?
│ └─ SÍ → Opción 1 (Base Auto)
│ └─ NO ↓
│
├─ ¿Necesitas transformaciones/validaciones?
│ └─ SÍ → ¿Solo en algunas propiedades?
│ ├─ SÍ → Opción 3 (Híbrido)
│ └─ NO → Opción 2 (Manual)
│ └─ NO → Opción 1 (Base Auto)
│
└─ ¿Performance es crítico? (microsegundos)
└─ SÍ → Opción 2 (Manual)
└─ NO → Opción 1 (Base Auto)
💡 Recomendación General: Empieza con Opción 1 (Base Auto), cambia a Opción 3 si necesitas lógica especial en algunos campos.
[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObj : OracleUdtBase // ← Hereda
{
[OracleObjectMapping("SZTYOTIOL_TERM_CODE")]
public string? TermCode { get; set; }
// ✅ Listo! No necesitas FromCustomObject/ToCustomObject
}[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObj : IOracleCustomType, INullable // ← Implementa
{
public void FromCustomObject(OracleConnection con, object pUdt)
{
OracleUdt.SetValue(con, pUdt, "SZTYOTIOL_TERM_CODE", TermCode);
// ✅ Control total línea por línea
}
}[OracleCustomTypeMapping("SZTYOTIOL_OBJ")]
public class TiraOracleObj : OracleUdtBase // ← Hereda
{
public override void FromCustomObject(OracleConnection con, object pUdt)
{
base.FromCustomObject(con, pUdt); // ← Automático
// Tu lógica manual adicional aquí
}
}🎉 ¡Elige la que mejor se adapte a tu caso!