diff --git a/EntityInjector.Core/EntityInjector.Core.csproj b/EntityInjector.Core/EntityInjector.Core.csproj
new file mode 100644
index 0000000..35bf720
--- /dev/null
+++ b/EntityInjector.Core/EntityInjector.Core.csproj
@@ -0,0 +1,16 @@
+
+
+
+ netstandard2.1
+ latest
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/EntityInjector.Core/Exceptions/EntityBindingException.cs b/EntityInjector.Core/Exceptions/EntityBindingException.cs
new file mode 100644
index 0000000..6674f25
--- /dev/null
+++ b/EntityInjector.Core/Exceptions/EntityBindingException.cs
@@ -0,0 +1,8 @@
+namespace EntityInjector.Core.Exceptions;
+
+public abstract class EntityBindingException(string message, Exception? inner = null)
+ : Exception(message, inner)
+{
+ public abstract int StatusCode { get; }
+ public virtual string? Description => Message;
+}
\ No newline at end of file
diff --git a/EntityInjector.Core/Exceptions/IExceptionMetadata.cs b/EntityInjector.Core/Exceptions/IExceptionMetadata.cs
new file mode 100644
index 0000000..3645b61
--- /dev/null
+++ b/EntityInjector.Core/Exceptions/IExceptionMetadata.cs
@@ -0,0 +1,7 @@
+namespace EntityInjector.Core.Exceptions;
+
+public interface IExceptionMetadata
+{
+ int StatusCode { get; }
+ string DefaultDescription { get; }
+}
\ No newline at end of file
diff --git a/EntityInjector.Route/Exceptions/Middleware/DefaultRouteBindingProblemDetailsFactory.cs b/EntityInjector.Core/Exceptions/Middleware/DefaultEntityBindingProblemDetailsFactory.cs
similarity index 53%
rename from EntityInjector.Route/Exceptions/Middleware/DefaultRouteBindingProblemDetailsFactory.cs
rename to EntityInjector.Core/Exceptions/Middleware/DefaultEntityBindingProblemDetailsFactory.cs
index ec9d452..3111195 100644
--- a/EntityInjector.Route/Exceptions/Middleware/DefaultRouteBindingProblemDetailsFactory.cs
+++ b/EntityInjector.Core/Exceptions/Middleware/DefaultEntityBindingProblemDetailsFactory.cs
@@ -1,11 +1,11 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace EntityInjector.Route.Exceptions.Middleware;
+namespace EntityInjector.Core.Exceptions.Middleware;
-public class DefaultRouteBindingProblemDetailsFactory : IRouteBindingProblemDetailsFactory
+public class DefaultEntityBindingProblemDetailsFactory : IEntityBindingProblemDetailsFactory
{
- public ProblemDetails Create(HttpContext context, RouteBindingException exception)
+ public ProblemDetails Create(HttpContext context, EntityBindingException exception)
{
return new ProblemDetails
{
diff --git a/EntityInjector.Core/Exceptions/Middleware/EntityBindingApplicationBuilderExtensions.cs b/EntityInjector.Core/Exceptions/Middleware/EntityBindingApplicationBuilderExtensions.cs
new file mode 100644
index 0000000..11cf499
--- /dev/null
+++ b/EntityInjector.Core/Exceptions/Middleware/EntityBindingApplicationBuilderExtensions.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Builder;
+
+namespace EntityInjector.Core.Exceptions.Middleware;
+
+public static class EntityBindingApplicationBuilderExtensions
+{
+ public static IApplicationBuilder UseEntityBinding(this IApplicationBuilder app)
+ {
+ return app.UseMiddleware();
+ }
+}
\ No newline at end of file
diff --git a/EntityInjector.Route/Exceptions/Middleware/RouteBindingExceptionMiddleware.cs b/EntityInjector.Core/Exceptions/Middleware/EntityBindingExceptionMiddleware.cs
similarity index 56%
rename from EntityInjector.Route/Exceptions/Middleware/RouteBindingExceptionMiddleware.cs
rename to EntityInjector.Core/Exceptions/Middleware/EntityBindingExceptionMiddleware.cs
index c91efb8..fe1906e 100644
--- a/EntityInjector.Route/Exceptions/Middleware/RouteBindingExceptionMiddleware.cs
+++ b/EntityInjector.Core/Exceptions/Middleware/EntityBindingExceptionMiddleware.cs
@@ -2,28 +2,30 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-namespace EntityInjector.Route.Exceptions.Middleware;
+namespace EntityInjector.Core.Exceptions.Middleware;
-public class RouteBindingExceptionMiddleware(
+public class EntityBindingExceptionMiddleware(
RequestDelegate next,
- ILogger logger,
- IRouteBindingProblemDetailsFactory? problemDetailsFactory = null)
+ ILogger logger,
+ IEntityBindingProblemDetailsFactory? problemDetailsFactory = null)
{
- private readonly IRouteBindingProblemDetailsFactory _problemDetailsFactory = problemDetailsFactory ?? new DefaultRouteBindingProblemDetailsFactory();
private static readonly JsonSerializerOptions JsonOptions = new()
{
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
+ private readonly IEntityBindingProblemDetailsFactory _problemDetailsFactory =
+ problemDetailsFactory ?? new DefaultEntityBindingProblemDetailsFactory();
+
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
- catch (RouteBindingException ex)
+ catch (EntityBindingException ex)
{
- logger.LogWarning(ex, "Route binding error: {Message}", ex.Message);
+ logger.LogWarning(ex, "Entity binding error: {Message}", ex.Message);
var problemDetails = _problemDetailsFactory.Create(context, ex);
@@ -34,5 +36,4 @@ public async Task Invoke(HttpContext context)
await context.Response.WriteAsync(json);
}
}
-
-}
+}
\ No newline at end of file
diff --git a/EntityInjector.Core/Exceptions/Middleware/EntityBindingServiceCollectionExtensions.cs b/EntityInjector.Core/Exceptions/Middleware/EntityBindingServiceCollectionExtensions.cs
new file mode 100644
index 0000000..6732ff1
--- /dev/null
+++ b/EntityInjector.Core/Exceptions/Middleware/EntityBindingServiceCollectionExtensions.cs
@@ -0,0 +1,14 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace EntityInjector.Core.Exceptions.Middleware;
+
+public static class EntityBindingServiceCollectionExtensions
+{
+ public static IServiceCollection AddEntityBinding(this IServiceCollection services)
+ {
+ // Register default formatter if user hasn't already
+ services.TryAddSingleton();
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/EntityInjector.Core/Exceptions/Middleware/IEntityBindingProblemDetailsFactory.cs b/EntityInjector.Core/Exceptions/Middleware/IEntityBindingProblemDetailsFactory.cs
new file mode 100644
index 0000000..5949e6d
--- /dev/null
+++ b/EntityInjector.Core/Exceptions/Middleware/IEntityBindingProblemDetailsFactory.cs
@@ -0,0 +1,9 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace EntityInjector.Core.Exceptions.Middleware;
+
+public interface IEntityBindingProblemDetailsFactory
+{
+ ProblemDetails Create(HttpContext context, EntityBindingException exception);
+}
\ No newline at end of file
diff --git a/EntityInjector.Core/Exceptions/StatusExceptions.cs b/EntityInjector.Core/Exceptions/StatusExceptions.cs
new file mode 100644
index 0000000..36b85e3
--- /dev/null
+++ b/EntityInjector.Core/Exceptions/StatusExceptions.cs
@@ -0,0 +1,85 @@
+using Microsoft.AspNetCore.Http;
+
+namespace EntityInjector.Core.Exceptions;
+
+public sealed class EntityNotFoundException(string entityName, object? id)
+ : EntityBindingException($"No {entityName} found for ID '{id}'."), IExceptionMetadata
+{
+ public string EntityName { get; } = entityName;
+ public object? Id { get; } = id;
+ public override int StatusCode => StatusCodes.Status404NotFound;
+ public string DefaultDescription => "The requested entity was not found.";
+}
+
+public sealed class MissingEntityAttributeException(string parameterName, string expectedAttribute)
+ : EntityBindingException($"Missing required {expectedAttribute} on action parameter '{parameterName}'."),
+ IExceptionMetadata
+{
+ public override int StatusCode => StatusCodes.Status400BadRequest;
+ public string DefaultDescription => "A required parameter attribute was missing.";
+}
+
+public sealed class UnsupportedBindingTypeException(Type targetType)
+ : EntityBindingException($"The type '{targetType.Name}' is not supported for route binding.")
+{
+ public override int StatusCode => StatusCodes.Status400BadRequest;
+
+ public Type TargetType { get; } = targetType;
+}
+
+public sealed class BindingReceiverNotRegisteredException(Type receiverType)
+ : EntityBindingException($"No binding receiver registered for type '{receiverType.FullName}'.")
+{
+ public override int StatusCode => StatusCodes.Status500InternalServerError;
+
+ public Type ReceiverType { get; } = receiverType;
+}
+
+public sealed class BindingReceiverContractException(string methodName, Type receiverType)
+ : EntityBindingException($"Expected method '{methodName}' not found on receiver type '{receiverType.Name}'.")
+{
+ public override int StatusCode => StatusCodes.Status500InternalServerError;
+
+ public string MethodName { get; } = methodName;
+ public Type ReceiverType { get; } = receiverType;
+}
+
+public sealed class UnexpectedBindingResultException(Type expected, Type? actual)
+ : EntityBindingException($"Expected result of type '{expected.Name}', but got '{actual?.Name ?? "null"}'.")
+{
+ public override int StatusCode => StatusCodes.Status500InternalServerError;
+
+ public Type ExpectedType { get; } = expected;
+ public Type? ActualType { get; } = actual;
+}
+
+public sealed class MissingEntityParameterException(string parameterName)
+ : EntityBindingException(
+ $"Route parameter '{parameterName}' was not found. Ensure it is correctly specified in the route."),
+ IExceptionMetadata
+{
+ public string ParameterName { get; } = parameterName;
+ public override int StatusCode => StatusCodes.Status400BadRequest;
+ public string DefaultDescription => "A required route parameter was missing.";
+}
+
+public sealed class InvalidEntityParameterFormatException(string parameterName, Type expectedType, Type actualType)
+ : EntityBindingException(
+ $"Route parameter '{parameterName}' is of type '{actualType.Name}', but type '{expectedType.Name}' was expected."),
+ IExceptionMetadata
+{
+ public string ParameterName { get; } = parameterName;
+ public Type ExpectedType { get; } = expectedType;
+ public Type ActualType { get; } = actualType;
+ public override int StatusCode => StatusCodes.Status422UnprocessableEntity;
+ public string DefaultDescription => "A route parameter was not in the expected format.";
+}
+
+public sealed class EmptyEntitySegmentListException(string parameterName)
+ : EntityBindingException($"Route parameter '{parameterName}' did not contain any valid string segments."),
+ IExceptionMetadata
+{
+ public string ParameterName { get; } = parameterName;
+ public override int StatusCode => StatusCodes.Status422UnprocessableEntity;
+ public string DefaultDescription => "The route parameter did not contain any valid values.";
+}
\ No newline at end of file
diff --git a/EntityInjector.Route/Middleware/Attributes/MetadataParsingHelper.cs b/EntityInjector.Core/Helpers/MetadataParsingHelper.cs
similarity index 87%
rename from EntityInjector.Route/Middleware/Attributes/MetadataParsingHelper.cs
rename to EntityInjector.Core/Helpers/MetadataParsingHelper.cs
index 2e1e133..ef9f5f6 100644
--- a/EntityInjector.Route/Middleware/Attributes/MetadataParsingHelper.cs
+++ b/EntityInjector.Core/Helpers/MetadataParsingHelper.cs
@@ -1,4 +1,4 @@
-namespace EntityInjector.Route.Middleware.Attributes;
+namespace EntityInjector.Core.Helpers;
public static class MetadataParsingHelper
{
diff --git a/EntityInjector.Route/Interfaces/IBindingModelDataReceiver.cs b/EntityInjector.Core/Interfaces/IBindingModelDataReceiver.cs
similarity index 88%
rename from EntityInjector.Route/Interfaces/IBindingModelDataReceiver.cs
rename to EntityInjector.Core/Interfaces/IBindingModelDataReceiver.cs
index 1c9f35d..99ed1fd 100644
--- a/EntityInjector.Route/Interfaces/IBindingModelDataReceiver.cs
+++ b/EntityInjector.Core/Interfaces/IBindingModelDataReceiver.cs
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Http;
-namespace EntityInjector.Route.Interfaces;
+namespace EntityInjector.Core.Interfaces;
public interface IBindingModelDataReceiver where TKey : notnull
{
diff --git a/EntityInjector.Property/Attributes/FromPropertyToEntityAttribute.cs b/EntityInjector.Property/Attributes/FromPropertyToEntityAttribute.cs
new file mode 100644
index 0000000..dae1f50
--- /dev/null
+++ b/EntityInjector.Property/Attributes/FromPropertyToEntityAttribute.cs
@@ -0,0 +1,10 @@
+using EntityInjector.Core.Helpers;
+
+namespace EntityInjector.Property.Attributes;
+
+[AttributeUsage(AttributeTargets.Property)]
+public class FromPropertyToEntityAttribute(string propertyName, string? metaData = null) : Attribute
+{
+ public readonly Dictionary MetaData = MetadataParsingHelper.ParseMetaData(metaData);
+ public readonly string PropertyName = propertyName;
+}
\ No newline at end of file
diff --git a/EntityInjector.Property/EntityInjector.Property.csproj b/EntityInjector.Property/EntityInjector.Property.csproj
new file mode 100644
index 0000000..7d056d2
--- /dev/null
+++ b/EntityInjector.Property/EntityInjector.Property.csproj
@@ -0,0 +1,28 @@
+
+
+
+ true
+ EntityInjector.Property
+ 1.0.0
+ Devies
+ John Johansson; Erik Jergéus
+ netstandard2.1
+ latest
+ enable
+ enable
+ A small library for injecting entities into proerties into Entity Framework contexts.
+ MIT
+ https://github.com/DeviesDevelopment/entity-injector
+ https://github.com/DeviesDevelopment/entity-injector
+ git
+
+
+
+
+
+
+
+
+
+
+
diff --git a/EntityInjector.Property/Filters/FromPropertyToEntityActionFilter.cs b/EntityInjector.Property/Filters/FromPropertyToEntityActionFilter.cs
new file mode 100644
index 0000000..30bd132
--- /dev/null
+++ b/EntityInjector.Property/Filters/FromPropertyToEntityActionFilter.cs
@@ -0,0 +1,143 @@
+using System.Collections;
+using System.Reflection;
+using EntityInjector.Core.Exceptions;
+using EntityInjector.Core.Interfaces;
+using EntityInjector.Property.Helpers;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Extensions.Logging;
+
+namespace EntityInjector.Property.Filters;
+
+public abstract class FromPropertyToEntityActionFilter(
+ IServiceProvider serviceProvider,
+ ILogger> logger)
+ : IAsyncActionFilter
+{
+ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ {
+ if (context.ActionArguments == null || context.ActionArguments.Count == 0)
+ {
+ await next();
+ return;
+ }
+
+ var toProcess = EntityBindingCollector.Collect(context.ActionArguments.Values, context.ModelState);
+
+ var groupedByType = toProcess
+ .GroupBy(info => (info.EntityType,
+ string.Join("&", info.MetaData.OrderBy(x => x.Key).Select(x => $"{x.Key}={x.Value}"))))
+ .Select(g => new { g.Key.EntityType, g.First().MetaData, Bindings = g.ToList() })
+ .ToList();
+
+ foreach (var group in groupedByType)
+ {
+ var allIds = group.Bindings.SelectMany(b => b.Ids).Distinct().ToList();
+ if (allIds.Count == 0) continue;
+
+ var fetchedEntities = await GetEntitiesAsync(allIds, context.HttpContext, group.EntityType, group.MetaData);
+
+ foreach (var binding in group.Bindings)
+ EntityPopulator.Populate(
+ binding.TargetObject,
+ binding.TargetProperty,
+ binding.EntityType,
+ binding.Ids.Cast