Skip to content

Commit

Permalink
Merge pull request #68 from jwienekamp/enumerablefield
Browse files Browse the repository at this point in the history
Add EnumerableField<T>
  • Loading branch information
huysentruitw authored Aug 23, 2022
2 parents e673b76 + 2edb520 commit acdef8b
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 21 deletions.
43 changes: 30 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,22 @@ class SomeFunctionResultItem
}
```

### Define models with an IEnumerable

```csharp
class SomeFunctionResult
{
[SapName("RES_ITEMS")]
public IEnumerable<SomeFunctionResultItem> Items { get; set; }
}

class SomeFunctionResultItem
{
[SapName("ITM_NAME")]
public string Name { get; set; }
}
```

### Exclude properties from mapping

```csharp
Expand Down Expand Up @@ -320,19 +336,20 @@ For each input and output model type, the library builds and caches a mapping fu

SAP RFC parameter types don't have to be specified as they're converted by convention. Here's an overview of supported type mappings:

| C# type | SAP RFC type | Remarks
|:---------- |:---------------------------- |:---
| `int` | RFCTYPE_INT | 4-byte integer
| `long` | RFCTYPE_INT8 | 8-byte integer
| `double` | RFCTYPE_FLOAT | Floating point, double precision
| `decimal` | RFCTYPE_BCD |
| `string` | RFCTYPE_STRING / RFCTYPE_CHAR / ... | Gets a data field as string
| `byte[]` | RFCTYPE_BYTE | Raw binary data, fixed length. Has to be used in conjunction with the `[SapBufferLength]`-attribute
| `char[]` | RFCTYPE_CHAR | Char data, fixed length. Has to be used in conjunction with the `[SapBufferLength]`-attribute
| `DateTime?` | RFCTYPE_DATE | Only the day, month and year value is used
| `TimeSpan?` | RFCTYPE_TIME | Only the hour, minute and second value is used
| `T` | RFCTYPE_STRUCTURE | Structures are constructed from nested objects (T) in the input or output model (see [example](#define-models-with-a-nested-structure))
| `Array<T>` | RFCTYPE_TABLE | Tables are constructed from arrays of nested objects (T) in the input or output model (see [example](#define-models-with-a-nested-table))
| C# type | SAP RFC type | Remarks
|:--------------- |:---------------------------- |:---
| `int` | RFCTYPE_INT | 4-byte integer
| `long` | RFCTYPE_INT8 | 8-byte integer
| `double` | RFCTYPE_FLOAT | Floating point, double precision
| `decimal` | RFCTYPE_BCD |
| `string` | RFCTYPE_STRING / RFCTYPE_CHAR / ... | Gets a data field as string
| `byte[]` | RFCTYPE_BYTE | Raw binary data, fixed length. Has to be used in conjunction with the `[SapBufferLength]`-attribute
| `char[]` | RFCTYPE_CHAR | Char data, fixed length. Has to be used in conjunction with the `[SapBufferLength]`-attribute
| `DateTime?` | RFCTYPE_DATE | Only the day, month and year value is used
| `TimeSpan?` | RFCTYPE_TIME | Only the hour, minute and second value is used
| `T` | RFCTYPE_STRUCTURE | Structures are constructed from nested objects (T) in the input or output model (see [example](#define-models-with-a-nested-structure))
| `Array<T>` | RFCTYPE_TABLE | Tables are constructed from arrays of nested objects (T) in the input or output model (see [example](#define-models-with-a-nested-table))
| `IEnumerable<T>` | RFCTYPE_TABLE | Tables returned as IEnumerable<T>. Yields elements one-by-one for better memory management when handling large datasets

## Connection pooling

Expand Down
99 changes: 99 additions & 0 deletions src/SapNwRfc/Internal/Fields/EnumerableField.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using SapNwRfc.Internal.Interop;

namespace SapNwRfc.Internal.Fields
{
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global", Justification = "Reflection use")]
internal sealed class EnumerableField<TItem> : Field<IEnumerable<TItem>>
{
public EnumerableField(string name, IEnumerable<TItem> value)
: base(name, value)
{
}

public override void Apply(RfcInterop interop, IntPtr dataHandle)
{
if (Value == null)
return;

RfcResultCode resultCode = interop.GetTable(
dataHandle: dataHandle,
name: Name,
out IntPtr tableHandle,
out RfcErrorInfo errorInfo);

resultCode.ThrowOnError(errorInfo);

foreach (TItem row in Value)
{
IntPtr lineHandle = interop.AppendNewRow(tableHandle, out errorInfo);
errorInfo.ThrowOnError();
InputMapper.Apply(interop, lineHandle, row);
}
}

public static EnumerableField<T> Extract<T>(RfcInterop interop, IntPtr dataHandle, string name)
{
RfcResultCode resultCode = interop.GetTable(
dataHandle: dataHandle,
name: name,
tableHandle: out IntPtr tableHandle,
errorInfo: out RfcErrorInfo errorInfo);

resultCode.ThrowOnError(errorInfo);

resultCode = interop.GetRowCount(
tableHandle: tableHandle,
rowCount: out uint rowCount,
errorInfo: out errorInfo);

resultCode.ThrowOnError(errorInfo);

if (rowCount == 0)
return new EnumerableField<T>(name, Enumerable.Empty<T>());

IEnumerable<T> rows = YieldTableRows<T>(interop, tableHandle);

return new EnumerableField<T>(name, rows);
}

public static IEnumerable<T> YieldTableRows<T>(RfcInterop interop, IntPtr tableHandle)
{
RfcResultCode moveFirstResultCode = interop.MoveToFirstRow(
tableHandle: tableHandle,
errorInfo: out RfcErrorInfo moveFirstErrorInfo);

if (moveFirstResultCode == RfcResultCode.RFC_TABLE_MOVE_BOF)
yield break;

moveFirstResultCode.ThrowOnError(moveFirstErrorInfo);

while (true)
{
IntPtr rowHandle = interop.GetCurrentRow(
tableHandle: tableHandle,
errorInfo: out RfcErrorInfo errorInfo);

errorInfo.ThrowOnError();

yield return OutputMapper.Extract<T>(interop, rowHandle);

RfcResultCode moveNextResultCode = interop.MoveToNextRow(
tableHandle: tableHandle,
errorInfo: out RfcErrorInfo moveNextErrorInfo);

if (moveNextResultCode == RfcResultCode.RFC_TABLE_MOVE_EOF)
yield break;

moveNextResultCode.ThrowOnError(moveNextErrorInfo);
}
}

[ExcludeFromCodeCoverage]
public override string ToString()
=> string.Join(Environment.NewLine, Value.Select((row, index) => $"[{index}] {row}"));
}
}
8 changes: 8 additions & 0 deletions src/SapNwRfc/Internal/InputMapper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Linq;
using System.Linq.Expressions;
Expand Down Expand Up @@ -129,6 +130,13 @@ private static Expression BuildApplyExpressionForProperty(
Type tableFieldType = typeof(TableField<>).MakeGenericType(propertyInfo.PropertyType.GetElementType());
fieldConstructor = tableFieldType.GetConstructor(new[] { typeof(string), propertyInfo.PropertyType });
}
else if (propertyInfo.PropertyType.GetInterfaces().Contains(typeof(IEnumerable)))
{
Type elementType = propertyInfo.PropertyType.GetGenericArguments()[0];

Type tableFieldType = typeof(EnumerableField<>).MakeGenericType(elementType);
fieldConstructor = tableFieldType.GetConstructor(new[] { typeof(string), propertyInfo.PropertyType });
}
else if (!propertyInfo.PropertyType.IsPrimitive)
{
// new RfcStructureField<T>(name, (T)value);
Expand Down
10 changes: 10 additions & 0 deletions src/SapNwRfc/Internal/OutputMapper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
Expand Down Expand Up @@ -120,10 +121,19 @@ private static Expression BuildExtractExpressionForProperty(
else if (propertyInfo.PropertyType.IsArray)
{
Type elementType = propertyInfo.PropertyType.GetElementType();

extractMethod = GetMethodInfo(() => TableField<object>.Extract<object>(default, default, default))
.GetGenericMethodDefinition()
.MakeGenericMethod(elementType);
}
else if (propertyInfo.PropertyType.GetInterfaces().Contains(typeof(IEnumerable)))
{
Type elementType = propertyInfo.PropertyType.GetGenericArguments()[0];

extractMethod = GetMethodInfo(() => EnumerableField<object>.Extract<object>(default, default, default))
.GetGenericMethodDefinition()
.MakeGenericMethod(elementType);
}
else if (!propertyInfo.PropertyType.IsPrimitive)
{
extractMethod = GetMethodInfo(() => StructureField<object>.Extract<object>(default, default, default))
Expand Down
42 changes: 34 additions & 8 deletions src/SapNwRfc/Pooling/SapPooledConnection.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using SapNwRfc.Exceptions;
Expand All @@ -9,6 +10,8 @@ namespace SapNwRfc.Pooling
/// </summary>
public sealed class SapPooledConnection : ISapPooledConnection
{
private readonly List<ISapFunction> _functions = new List<ISapFunction>();

private readonly ISapConnectionPool _pool;
private ISapConnection _connection = null;
private bool _disposed = false;
Expand Down Expand Up @@ -39,6 +42,13 @@ public void Dispose()
if (_disposed)
return;

// manually dispose any open function on current connection
// when it gets returned to pool
foreach (ISapFunction function in _functions)
{
function.Dispose();
}

if (_connection != null)
{
_pool.ReturnConnection(_connection);
Expand Down Expand Up @@ -99,8 +109,13 @@ public TOutput InvokeFunction<TOutput>(string name, CancellationToken cancellati

try
{
using (ISapFunction function = _connection.CreateFunction(name))
return function.Invoke<TOutput>();
// no using here to prevent function from being disposed
// when possibly an IEnumerable is used in TOutput
// and rows are accessed after InvokeFunction
ISapFunction function = _connection.CreateFunction(name);
_functions.Add(function);

return function.Invoke<TOutput>();
}
catch (SapCommunicationFailedException)
{
Expand All @@ -109,8 +124,11 @@ public TOutput InvokeFunction<TOutput>(string name, CancellationToken cancellati

// Retry invocation with new connection from the pool
_connection = _pool.GetConnection(cancellationToken);
using (ISapFunction function = _connection.CreateFunction(name))
return function.Invoke<TOutput>();

ISapFunction function = _connection.CreateFunction(name);
_functions.Add(function);

return function.Invoke<TOutput>();
}
}

Expand All @@ -121,8 +139,13 @@ public TOutput InvokeFunction<TOutput>(string name, object input, CancellationTo

try
{
using (ISapFunction function = _connection.CreateFunction(name))
return function.Invoke<TOutput>(input);
// no using here to prevent function from being disposed
// when possibly an IEnumerable is used in TOutput
// and rows are accessed after InvokeFunction
ISapFunction function = _connection.CreateFunction(name);
_functions.Add(function);

return function.Invoke<TOutput>(input);
}
catch (SapCommunicationFailedException)
{
Expand All @@ -131,8 +154,11 @@ public TOutput InvokeFunction<TOutput>(string name, object input, CancellationTo

// Retry invocation with new connection from the pool
_connection = _pool.GetConnection(cancellationToken);
using (ISapFunction function = _connection.CreateFunction(name))
return function.Invoke<TOutput>(input);

ISapFunction function = _connection.CreateFunction(name);
_functions.Add(function);

return function.Invoke<TOutput>(input);
}
}
}
Expand Down
67 changes: 67 additions & 0 deletions tests/SapNwRfc.Tests/Internal/InputMapperTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AutoFixture;
using FluentAssertions;
Expand Down Expand Up @@ -331,6 +332,62 @@ public void Apply_NullArray_ShouldNotMapAsTable()
_interopMock.Verify(x => x.GetTable(DataHandle, It.IsAny<string>(), out tableHandle, out errorInfo), Times.Never);
}

[Fact]
public void Apply_Enumerable_ShouldMapAsTable()
{
// Arrange
RfcErrorInfo errorInfo;
var model = new EnumerableModel { SomeEnumerable = Fixture.CreateMany<EnumerableElement>(2) };

// Act
InputMapper.Apply(_interopMock.Object, DataHandle, model);

// Assert
IntPtr tableHandle;
_interopMock.Verify(x => x.GetTable(DataHandle, "SOMEENUMERABLE", out tableHandle, out errorInfo), Times.Once);
}

[Fact]
public void Apply_Enumerable_ShouldMapRowsAndValues()
{
// Arrange
const int numberOfRows = 5;
var tableHandle = (IntPtr)1235;
var lineHandle = (IntPtr)2245;
RfcErrorInfo errorInfo;
_interopMock.Setup(x => x.GetTable(DataHandle, "SOMEENUMERABLE", out tableHandle, out errorInfo));
_interopMock.Setup(x => x.AppendNewRow(It.IsAny<IntPtr>(), out errorInfo)).Returns(lineHandle);
var model = new EnumerableModel { SomeEnumerable = Fixture.CreateMany<EnumerableElement>(numberOfRows) };

// Act
InputMapper.Apply(_interopMock.Object, DataHandle, model);

// Assert
_interopMock.Verify(x => x.AppendNewRow(tableHandle, out errorInfo), Times.Exactly(numberOfRows));
foreach (EnumerableElement element in model.SomeEnumerable)
{
var length = (uint)element.Value.Length;
_interopMock.Verify(
x => x.SetString(lineHandle, "VALUE", element.Value, length, out errorInfo),
Times.Once);
}
}

[Fact]
public void Apply_NullEnumerable_ShouldNotMapAsTable()
{
// Arrange
RfcErrorInfo errorInfo;
var model = new EnumerableModel();

// Act
InputMapper.Apply(_interopMock.Object, DataHandle, model);

// Assert
IntPtr tableHandle;
_interopMock.Verify(x => x.GetTable(DataHandle, It.IsAny<string>(), out tableHandle, out errorInfo), Times.Never);
}

[Fact]
public void Apply_Structure_ShouldMapAsStructure()
{
Expand Down Expand Up @@ -412,6 +469,16 @@ private sealed class ArrayElement
public string Value { get; set; } = "123";
}

private sealed class EnumerableModel
{
public IEnumerable<EnumerableElement> SomeEnumerable { get; set; }
}

private sealed class EnumerableElement
{
public string Value { get; set; } = "234";
}

private sealed class StructureModel
{
public Structure Structure { get; set; }
Expand Down
Loading

0 comments on commit acdef8b

Please sign in to comment.