Skip to content

Commit

Permalink
Support sproc input/output parameters on non-concurrency-token proper…
Browse files Browse the repository at this point in the history
…ties

Closes dotnet#28704
  • Loading branch information
roji committed Sep 9, 2022
1 parent 49625dd commit 53f8f7b
Show file tree
Hide file tree
Showing 15 changed files with 489 additions and 171 deletions.
75 changes: 69 additions & 6 deletions src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -436,12 +436,69 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy
RelationalStrings.StoredProcedureParameterNotFound(
parameter.PropertyName, entityType.DisplayName(), storeObjectIdentifier.DisplayName()));
}

if (storeObjectIdentifier.StoreObjectType == StoreObjectType.InsertStoredProcedure)
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureOriginalValueParameterOnInsert(
parameter.Name, storeObjectIdentifier.DisplayName()));
}
}
else if (!properties.TryGetAndRemove(parameter.PropertyName, out property))
else
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureParameterNotFound(
parameter.PropertyName, entityType.DisplayName(), storeObjectIdentifier.DisplayName()));
if (!properties.TryGetAndRemove(parameter.PropertyName, out property))
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureParameterNotFound(
parameter.PropertyName, entityType.DisplayName(), storeObjectIdentifier.DisplayName()));
}

if (storeObjectIdentifier.StoreObjectType == StoreObjectType.DeleteStoredProcedure)
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureCurrentValueParameterOnDelete(
parameter.Name, storeObjectIdentifier.DisplayName()));
}

if (parameter.Direction.HasFlag(ParameterDirection.Input))
{
switch (storeObjectIdentifier.StoreObjectType)
{
case StoreObjectType.InsertStoredProcedure:
if (property.GetBeforeSaveBehavior() != PropertySaveBehavior.Save)
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureInputParameterForInsertNonSaveProperty(
parameter.Name,
storeObjectIdentifier.DisplayName(),
parameter.PropertyName,
entityType.DisplayName(),
property.GetBeforeSaveBehavior()));
}
break;

case StoreObjectType.UpdateStoredProcedure:
if (property.GetAfterSaveBehavior() != PropertySaveBehavior.Save)
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureInputParameterForUpdateNonSaveProperty(
parameter.Name,
storeObjectIdentifier.DisplayName(),
parameter.PropertyName,
entityType.DisplayName(),
property.GetAfterSaveBehavior()));
}

break;

case StoreObjectType.DeleteStoredProcedure:
break;

default:
Check.DebugFail("Unexpected stored procedure type: " + storeObjectIdentifier.StoreObjectType);
break;
}
}
}
}

Expand Down Expand Up @@ -578,8 +635,14 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy
|| sproc.FindRowsAffectedParameter() != null
|| sproc.FindRowsAffectedResultColumn() != null)
{
if (originalValueProperties.Values.FirstOrDefault(p => p.IsConcurrencyToken) is { } missedConcurrencyToken
&& storeObjectIdentifier.StoreObjectType != StoreObjectType.InsertStoredProcedure)
if (storeObjectIdentifier.StoreObjectType == StoreObjectType.InsertStoredProcedure)
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureRowsAffectedForInsert(
storeObjectIdentifier.DisplayName()));
}

if (originalValueProperties.Values.FirstOrDefault(p => p.IsConcurrencyToken) is { } missedConcurrencyToken)
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureConcurrencyTokenNotMapped(
Expand Down
40 changes: 40 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,9 @@
<data name="StoredProcedureConcurrencyTokenNotMapped" xml:space="preserve">
<value>The entity type '{entityType}' is mapped to the stored procedure '{sproc}', but the concurrency token '{token}' is not mapped to any original value parameter.</value>
</data>
<data name="StoredProcedureCurrentValueParameterOnDelete" xml:space="preserve">
<value>Current value parameter '{parameter}' is not allowed on delete stored procedure '{sproc}'. Use HasOriginalValueParameter() instead.</value>
</data>
<data name="StoredProcedureDeleteNonKeyProperty" xml:space="preserve">
<value>The property '{entityType}.{property}' is mapped to a parameter of the stored procedure '{sproc}', but only concurrency token and key properties are supported for Delete stored procedures.</value>
</data>
Expand Down Expand Up @@ -1006,12 +1009,21 @@
<data name="StoredProcedureGeneratedPropertiesNotMapped" xml:space="preserve">
<value>The entity type '{entityType}' is mapped to the stored procedure '{sproc}', however the store-generated properties {properties} are not mapped to any output parameter or result column.</value>
</data>
<data name="StoredProcedureInputParameterForInsertNonSaveProperty" xml:space="preserve">
<value>Input parameter '{parameter}' of insert stored procedure '{sproc}' is mapped to property '{property}' of entity type '{entityType}', but that property is configured with BeforeSaveBehavior '{behavior}', and so cannot be saved on insert.</value>
</data>
<data name="StoredProcedureInputParameterForUpdateNonSaveProperty" xml:space="preserve">
<value>Input parameter '{parameter}' of update stored procedure '{sproc}' is mapped to property '{property}' of entity type '{entityType}', but that property is configured with AfterSaveBehavior '{behavior}', and so cannot be saved on update. You may need to use HasOriginalValueParameter() instead of HasParameter().</value>
</data>
<data name="StoredProcedureKeyless" xml:space="preserve">
<value>The keyless entity type '{entityType}' was configured to use '{sproc}'. An entity type requires a primary key to be able to be mapped to a stored procedure.</value>
</data>
<data name="StoredProcedureNoName" xml:space="preserve">
<value>The entity type '{entityType}' was configured to use '{sproc}', but the store name was not specified. Configure the stored procedure name explicitly.</value>
</data>
<data name="StoredProcedureOriginalValueParameterOnInsert" xml:space="preserve">
<value>Original value parameter '{parameter}' is not allowed on insert stored procedure '{sproc}'. Use HasParameter() instead.</value>
</data>
<data name="StoredProcedureOutputParameterConflict" xml:space="preserve">
<value>The property '{entityType}.{property}' is mapped to an output parameter of the stored procedure '{sproc}', but it is also mapped to an output original value output parameter. A store-generated property can only be mapped to one output parameter.</value>
</data>
Expand Down Expand Up @@ -1048,6 +1060,9 @@
<data name="StoredProcedureRowsAffectedNotPopulated" xml:space="preserve">
<value>Stored procedure '{sproc}' was configured with a rows affected output parameter or return value, but a valid value was not found when executing the procedure.</value>
</data>
<data name="StoredProcedureRowsAffectedForInsert" xml:space="preserve">
<value>A rows affected parameter, result column or return value cannot be configured on stored procedure '{sproc}' because it is used for insertion. Rows affected values are only allowed on stored procedures performing updating or deletion.</value>
</data>
<data name="StoredProcedureRowsAffectedReturnConflictingParameter" xml:space="preserve">
<value>The stored procedure '{sproc}' cannot be configured to return the rows affected because a rows affected parameter or a rows affected result column for this stored procedure already exists.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected override void Consume(RelationalDataReader reader)
bool? onResultSet = null;
var hasOutputParameters = false;

for (; commandIndex < ResultSetMappings.Count; commandIndex++)
while (commandIndex < ResultSetMappings.Count)
{
var resultSetMapping = ResultSetMappings[commandIndex];

Expand All @@ -63,10 +63,14 @@ protected override void Consume(RelationalDataReader reader)
? lastHandledCommandIndex == commandIndex
: lastHandledCommandIndex > commandIndex, "Bad handling of ResultSetMapping and command indexing");

commandIndex = lastHandledCommandIndex;
commandIndex = lastHandledCommandIndex + 1;

onResultSet = reader.DbDataReader.NextResult();
}
else
{
commandIndex++;
}

if (resultSetMapping.HasFlag(ResultSetMapping.HasOutputParameters))
{
Expand Down Expand Up @@ -158,7 +162,7 @@ protected override async Task ConsumeAsync(
bool? onResultSet = null;
var hasOutputParameters = false;

for (; commandIndex < ResultSetMappings.Count; commandIndex++)
while (commandIndex < ResultSetMappings.Count)
{
var resultSetMapping = ResultSetMappings[commandIndex];

Expand All @@ -177,10 +181,14 @@ protected override async Task ConsumeAsync(
? lastHandledCommandIndex == commandIndex
: lastHandledCommandIndex > commandIndex, "Bad handling of ResultSetMapping and command indexing");

commandIndex = lastHandledCommandIndex;
commandIndex = lastHandledCommandIndex + 1;

onResultSet = await reader.DbDataReader.NextResultAsync(cancellationToken).ConfigureAwait(false);
}
else
{
commandIndex++;
}

if (resultSetMapping.HasFlag(ResultSetMapping.HasOutputParameters))
{
Expand Down
5 changes: 4 additions & 1 deletion src/EFCore.Relational/Update/ModificationCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -523,9 +523,12 @@ void HandleColumnModification(IColumnMappingBase columnMapping)
&& (isKey
|| storedProcedureParameter is { ForOriginalValue: true }
|| (property.IsConcurrencyToken && storedProcedureParameter is null));

// Store-generated properties generally need to be read back (unless we're deleting).
// One exception is if the property is mapped to a non-output parameter.
var readValue = state != EntityState.Deleted
&& entry.IsStoreGenerated(property)
&& storedProcedureParameter is null or { ForOriginalValue: false };
&& (storedProcedureParameter is null || storedProcedureParameter.Direction.HasFlag(ParameterDirection.Output));

ColumnValuePropagator? columnPropagator = null;
sharedTableColumnMap?.TryGetValue(column.Name, out columnPropagator);
Expand Down
26 changes: 17 additions & 9 deletions src/EFCore.Relational/Update/ReaderModificationCommandBatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,13 @@ protected virtual void AddParameters(IReadOnlyModificationCommand modificationCo
Check.DebugAssert(!modificationCommand.ColumnModifications.Any(m => m.Column is IStoreStoredProcedureReturnValue)
|| modificationCommand.ColumnModifications[0].Column is IStoreStoredProcedureReturnValue,
"ResultValue column modification in non-first position");
foreach (var columnModification in modificationCommand.ColumnModifications)

var modifications = modificationCommand.StoreStoredProcedure is null
? modificationCommand.ColumnModifications
: modificationCommand.ColumnModifications.Where(
c => c.Column is IStoreStoredProcedureParameter or IStoreStoredProcedureReturnValue);

foreach (var columnModification in modifications)
{
AddParameter(columnModification);
}
Expand All @@ -288,13 +294,17 @@ protected virtual void AddParameter(IColumnModification columnModification)
_ => ParameterDirection.Input
};

// For in/out parameters, both UseCurrentValueParameter and UseOriginalValueParameter are true, but we only want to add a single
// parameter. This will happen below.
if (columnModification.UseCurrentValueParameter && direction != ParameterDirection.InputOutput)
// For the case where the same modification has both current and original value parameters, and corresponds to an in/out parameter,
// we only want to add a single parameter. This will happen below.
if (columnModification.UseCurrentValueParameter
&& !(columnModification.UseOriginalValueParameter && direction == ParameterDirection.InputOutput))
{
AddParameterCore(columnModification.ParameterName, direction == ParameterDirection.Output
? null
: columnModification.Value);
AddParameterCore(
columnModification.ParameterName, columnModification.UseCurrentValue
? columnModification.Value
: direction == ParameterDirection.InputOutput
? DBNull.Value
: null);
}

if (columnModification.UseOriginalValueParameter)
Expand All @@ -313,8 +323,6 @@ void AddParameterCore(string name, object? value)
columnModification.IsNullable,
direction);

// TODO: As an alternative, don't add output-only parameters to ParameterValues at all.
// But that means we can't check values exist for input parameters in RelationalParameterBase.AddDbParameter
ParameterValues.Add(name, value);

_pendingParameters++;
Expand Down
Loading

0 comments on commit 53f8f7b

Please sign in to comment.