Skip to content

Commit

Permalink
Add sproc support for Output and InputOutput parameters
Browse files Browse the repository at this point in the history
Add sproc support to runtime and compiled models
Add sproc support to relational model

Part of #245
  • Loading branch information
AndriySvyryd committed Jul 19, 2022
1 parent a28a6b7 commit 4faaa63
Show file tree
Hide file tree
Showing 14 changed files with 239 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -357,6 +358,54 @@ public static bool CanSetIsFixedLength(
bool fromDataAnnotation = false)
=> propertyBuilder.CanSetAnnotation(RelationalAnnotationNames.IsFixedLength, fixedLength, fromDataAnnotation);

/// <summary>
/// Sets the direction of the stored procedure parameter.
/// </summary>
/// <param name="propertyBuilder">The builder for the property being configured.</param>
/// <param name="direction">The direction.</param>
/// <param name="storeObject">The identifier of the stored procedure.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>
/// The same builder instance if the configuration was applied,
/// <see langword="null" /> otherwise.
/// </returns>
public static IConventionPropertyBuilder? HasDirection(
this IConventionPropertyBuilder propertyBuilder,
ParameterDirection direction,
in StoreObjectIdentifier storeObject,
bool fromDataAnnotation = false)
{
if (!propertyBuilder.CanSetDirection(direction, storeObject, fromDataAnnotation))
{
return null;
}

propertyBuilder.Metadata.SetDirection(direction, storeObject, fromDataAnnotation);
return propertyBuilder;
}

/// <summary>
/// Returns a value indicating whether the given direction can be configured on the corresponding stored procedure parameter.
/// </summary>
/// <param name="propertyBuilder">The builder for the property being configured.</param>
/// <param name="direction">The direction.</param>
/// <param name="storeObject">The identifier of the stored procedure.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns><see langword="true" /> if the property can be mapped to the given column.</returns>
public static bool CanSetDirection(
this IConventionPropertyBuilder propertyBuilder,
ParameterDirection direction,
in StoreObjectIdentifier storeObject,
bool fromDataAnnotation = false)
{
var overrides = (IConventionRelationalPropertyOverrides?)RelationalPropertyOverrides.Find(
propertyBuilder.Metadata, storeObject);
return overrides == null
|| (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention)
.Overrides(overrides.GetDirectionConfigurationSource())
|| overrides.Direction == direction;
}

/// <summary>
/// Configures the default value expression for the column that the property maps to when targeting a
/// relational database.
Expand Down
40 changes: 40 additions & 0 deletions src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using IColumnMapping = Microsoft.EntityFrameworkCore.Metadata.IColumnMapping;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -1100,6 +1102,44 @@ private static bool IsOptionalSharingDependent(
return optional ?? (entityType.BaseType != null && entityType.FindDiscriminatorProperty() != null);
}

/// <summary>
/// Gets the direction of the corresponding stored procedure parameter.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="storeObject">The identifier of the stored procedure containing the parameter.</param>
/// <returns>
/// The direction of the corresponding stored procedure parameter.
/// </returns>
public static ParameterDirection GetDirection(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject)
=> property.FindOverrides(storeObject)?.Direction ?? ParameterDirection.Input;

/// <summary>
/// Sets the direction of the corresponding stored procedure parameter.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="direction">The direction to set.</param>
/// <param name="storeObject">The identifier of the stored procedure containing the parameter.</param>
public static void SetDirection(
this IMutableProperty property,
ParameterDirection direction,
in StoreObjectIdentifier storeObject)
=> property.GetOrCreateOverrides(storeObject).Direction = direction;

/// <summary>
/// Sets the direction of the corresponding stored procedure parameter.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="direction">The direction to set.</param>
/// <param name="storeObject">The identifier of the stored procedure containing the parameter.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured value.</returns>
public static ParameterDirection? SetDirection(
this IConventionProperty property,
ParameterDirection direction,
in StoreObjectIdentifier storeObject,
bool fromDataAnnotation = false)
=> property.GetOrCreateOverrides(storeObject, fromDataAnnotation).SetDirection(direction, fromDataAnnotation);

/// <summary>
/// Returns the comment for the column this property is mapped to.
/// </summary>
Expand Down
38 changes: 32 additions & 6 deletions src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

Expand Down Expand Up @@ -365,13 +366,38 @@ private static void ValidateSproc(IStoredProcedure sproc, string mappingStrategy
parameter, entityType.DisplayName(), storeObjectIdentifier.DisplayName()));
}

if (storeObjectIdentifier.StoreObjectType == StoreObjectType.DeleteStoredProcedure
&& !property.IsPrimaryKey()
&& !property.IsConcurrencyToken)
switch (storeObjectIdentifier.StoreObjectType)
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureDeleteNonKeyProperty(
entityType.DisplayName(), parameter, storeObjectIdentifier.DisplayName()));
case StoreObjectType.InsertStoredProcedure:
if (property.GetDirection(storeObjectIdentifier) != ParameterDirection.Input
&& (property.ValueGenerated & ValueGenerated.OnAdd) == 0)
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureOutputParameterNotGenerated(
entityType.DisplayName(), parameter, storeObjectIdentifier.DisplayName()));
}

break;
case StoreObjectType.DeleteStoredProcedure:
if (!property.IsPrimaryKey()
&& !property.IsConcurrencyToken)
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureDeleteNonKeyProperty(
entityType.DisplayName(), parameter, storeObjectIdentifier.DisplayName()));
}

break;
case StoreObjectType.UpdateStoredProcedure:
if (property.GetDirection(storeObjectIdentifier) != ParameterDirection.Input
&& (property.ValueGenerated & ValueGenerated.OnUpdate) == 0)
{
throw new InvalidOperationException(
RelationalStrings.StoredProcedureOutputParameterNotGenerated(
entityType.DisplayName(), parameter, storeObjectIdentifier.DisplayName()));
}

break;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Data;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Metadata.Builders;
Expand Down Expand Up @@ -68,6 +69,36 @@ public virtual StoredProcedureParameterBuilder HasName(string? name)
return this;
}

/// <summary>
/// Configures the stored procedure parameter as both an input and output parameter.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see> and
/// <see href="https://aka.ms/efcore-docs-saving-data">Saving data with EF Core</see> for more information and examples.
/// </remarks>
/// <returns>The same builder instance so that further configuration calls can be chained.</returns>
public virtual StoredProcedureParameterBuilder IsInputOutput()
{
((IMutableRelationalPropertyOverrides)InternalOverrides).Direction = ParameterDirection.InputOutput;

return this;
}

/// <summary>
/// Configures the stored procedure parameter as an output parameter.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see> and
/// <see href="https://aka.ms/efcore-docs-saving-data">Saving data with EF Core</see> for more information and examples.
/// </remarks>
/// <returns>The same builder instance so that further configuration calls can be chained.</returns>
public virtual StoredProcedureParameterBuilder IsOutput()
{
((IMutableRelationalPropertyOverrides)InternalOverrides).Direction = ParameterDirection.Output;

return this;
}

/// <summary>
/// Adds or updates an annotation on the property for a specific stored procedure.
/// If an annotation with the key specified in <paramref name="annotation" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data;

namespace Microsoft.EntityFrameworkCore.Metadata;

/// <summary>
Expand Down Expand Up @@ -48,4 +50,20 @@ public interface IConventionRelationalPropertyOverrides : IReadOnlyRelationalPro
/// </summary>
/// <returns>The configuration source for <see cref="IReadOnlyRelationalPropertyOverrides.ColumnName" />.</returns>
ConfigurationSource? GetColumnNameConfigurationSource();

/// <summary>
/// Sets the direction of the stored procedure parameter.
/// </summary>
/// <param name="direction">The direction.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The configured value.</returns>
ParameterDirection? SetDirection(ParameterDirection? direction, bool fromDataAnnotation = false)
=> (ParameterDirection?)SetAnnotation(RelationalAnnotationNames.ParameterDirection, direction, fromDataAnnotation)?.Value;

/// <summary>
/// Returns the configuration source for <see cref="IReadOnlyRelationalPropertyOverrides.Direction" />.
/// </summary>
/// <returns>The configuration source for <see cref="IReadOnlyRelationalPropertyOverrides.Direction" />.</returns>
ConfigurationSource? GetDirectionConfigurationSource()
=> FindAnnotation(RelationalAnnotationNames.ParameterDirection)?.GetConfigurationSource();
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data;

namespace Microsoft.EntityFrameworkCore.Metadata;

/// <summary>
Expand All @@ -21,6 +23,15 @@ public interface IMutableRelationalPropertyOverrides : IReadOnlyRelationalProper
/// </summary>
new string? ColumnName { get; set; }

/// <summary>
/// Gets or sets the direction of the stored procedure parameter.
/// </summary>
new ParameterDirection? Direction
{
get => ((ParameterDirection?)this[RelationalAnnotationNames.ParameterDirection]);
set => SetAnnotation(RelationalAnnotationNames.ParameterDirection, value);
}

/// <summary>
/// Removes the column name override.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data;
using System.Text;

namespace Microsoft.EntityFrameworkCore.Metadata;
Expand Down Expand Up @@ -33,6 +34,12 @@ public interface IReadOnlyRelationalPropertyOverrides : IReadOnlyAnnotatable
/// </summary>
bool IsColumnNameOverridden { get; }

/// <summary>
/// Gets the direction of the stored procedure parameter.
/// </summary>
ParameterDirection? Direction
=> ((ParameterDirection?)this[RelationalAnnotationNames.ParameterDirection]);

/// <summary>
/// <para>
/// Creates a human-readable representation of the given metadata.
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@ private static void AddSqlQueries(RelationalModel databaseModel, IEntityType ent
}

Check.DebugAssert(definingType is not null, $"Could not find defining type for {entityType}");

var mappedType = entityType;
while (mappedType != null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data;

namespace Microsoft.EntityFrameworkCore.Metadata.Internal;

/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ public static class RelationalAnnotationNames
/// </summary>
public const string UpdateStoredProcedure = Prefix + "UpdateStoredProcedure";

/// <summary>
/// The name for mapped function name annotations.
/// </summary>
public const string ParameterDirection = Prefix + "ParameterDirection";

/// <summary>
/// The name for mapped sql query annotations.
/// </summary>
Expand Down

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

3 changes: 3 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,9 @@
<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="StoredProcedureOutputParameterNotGenerated" xml:space="preserve">
<value>The property '{entityType}.{property}' is mapped to an output parameter of the stored procedure '{sproc}', but it is not configured as store-generated. Either configure it as store-generated or don't configure the parameter as output.</value>
</data>
<data name="StoredProcedureOverrideMismatch" xml:space="preserve">
<value>The property '{propertySpecification}' has specific configuration for the stored procedure '{sproc}', but it isn't mapped to a parameter or a result column on that stored procedure. Remove the specific configuration, or map an entity type that contains this property to '{sproc}'.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2735,6 +2735,33 @@ public virtual void Detects_non_generated_update_stored_procedure_result_columns
modelBuilder);
}

[ConditionalFact]
public virtual void Detects_non_generated_insert_stored_procedure_output_parameter_in_TPC()
{
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity<Animal>()
.InsertUsingStoredProcedure(s => s.HasParameter(a => a.Name, p => p.IsOutput()))
.UseTpcMappingStrategy();
modelBuilder.Entity<Cat>();

VerifyError(
RelationalStrings.StoredProcedureOutputParameterNotGenerated(nameof(Animal), nameof(Animal.Name), "Animal_Insert"),
modelBuilder);
}

[ConditionalFact]
public virtual void Detects_non_generated_update_stored_procedure_input_output_parameter()
{
var modelBuilder = CreateConventionModelBuilder();
modelBuilder.Entity<Animal>()
.Ignore(a => a.FavoritePerson)
.UpdateUsingStoredProcedure(s => s.HasParameter(a => a.Id).HasParameter(a => a.Name, p => p.IsInputOutput()));

VerifyError(
RelationalStrings.StoredProcedureOutputParameterNotGenerated(nameof(Animal), nameof(Animal.Name), "Animal_Update"),
modelBuilder);
}

[ConditionalFact]
public virtual void Detects_delete_stored_procedure_result_columns_in_TPH()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1742,7 +1742,11 @@ protected virtual TestStoredProcedureParameterBuilder Wrap(StoredProcedureParame
public TestStoredProcedureParameterBuilder HasName(string? name)
=> Wrap(StoredProcedureParameterBuilder.HasName(name));

//public TestStoredProcedureParameterBuilder<TProperty> IsOutput(bool isOutput = true);
public TestStoredProcedureParameterBuilder IsOutput()
=> Wrap(StoredProcedureParameterBuilder.IsOutput());

public TestStoredProcedureParameterBuilder IsInputOutput()
=> Wrap(StoredProcedureParameterBuilder.IsInputOutput());
}

public class TestStoredProcedureResultColumnBuilder : IInfrastructure<StoredProcedureResultColumnBuilder>
Expand Down

0 comments on commit 4faaa63

Please sign in to comment.