Skip to content

Commit

Permalink
Provides explicit, unit tested support for OData actions. Closes #48.
Browse files Browse the repository at this point in the history
  • Loading branch information
Richard Beauchamp committed Jan 23, 2016
1 parent 453f3be commit d355430
Show file tree
Hide file tree
Showing 20 changed files with 509 additions and 69 deletions.
4 changes: 2 additions & 2 deletions Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
<Owners>Richard Beauchamp</Owners>
<Summary>Extends Swashbuckle with OData v4 support!</Summary>
<Description>Extends Swashbuckle with OData v4 support!</Description>
<ReleaseNotes>Custom Swagger Routes are supported with RESTier. Fixes #55.</ReleaseNotes>
<ReleaseNotes>Provides explicit, unit tested support for OData actions.</ReleaseNotes>
<ProjectUrl>https://github.com/rbeauchamp/Swashbuckle.OData</ProjectUrl>
<LicenseUrl>https://github.com/rbeauchamp/Swashbuckle.OData/blob/master/License.txt</LicenseUrl>
<Copyright>Copyright 2015</Copyright>
<Tags>Swashbuckle Swagger SwaggerUi OData Documentation Discovery Help WebApi AspNet AspNetWebApi Docs WebHost IIS</Tags>
<Version>2.11.1</Version>
<Version>2.12.0</Version>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Swashbuckle.OData\Swashbuckle.OData.csproj" />
Expand Down
12 changes: 12 additions & 0 deletions Swashbuckle.OData.Sample/App_Start/ODataConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,18 @@ private static IEdmModel GetFunctionsEdmModel()
.ReturnsCollectionFromEntitySet<Product>("Products")
.CollectionParameter<int>("Ids");

// An action bound to an entity set
// Accepts multiple action parameters
var action = productType.Collection.Action("Create");
action.ReturnsFromEntitySet<Product>("Products");
action.Parameter<string>("Name").OptionalParameter = false;
action.Parameter<double>("Price").OptionalParameter = false;
action.Parameter<MyEnum>("EnumValue").OptionalParameter = false;

// An action bound to an entity
productType.Action("Rate")
.Parameter<int>("Rating");

return builder.GetEdmModel();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IAp
new Tag { name = "Orders", description = "an ODataController resource" },
new Tag { name = "CustomersV1", description = "a versioned ODataController resource" },
new Tag { name = "Users", description = "a RESTier resource" },
new Tag { name = "Products", description = "demonstrates functions" }
new Tag { name = "Products", description = "demonstrates OData functions and actions" }
};
}
}
Expand Down
32 changes: 32 additions & 0 deletions Swashbuckle.OData.Sample/ODataControllers/ProductsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;
using System.Web.Http.Description;
using System.Web.OData;
Expand Down Expand Up @@ -123,6 +124,37 @@ public IQueryable<Product> ProductsWithIds([FromODataUri]int[] Ids)
return Data.Values.Where(p => Ids.Contains(p.Id)).AsQueryable();
}

/// <summary>
/// Creates a product. This action accepts parameters via an ODataActionParameters object.
/// </summary>
/// <param name="parameters">The OData action parameters.</param>
[HttpPost]
[ResponseType(typeof(Product))]
public IHttpActionResult Create(ODataActionParameters parameters)
{
var product = new Product
{
Id = Data.Values.Max(existingProduct => existingProduct.Id) + 1,
Name = (string)parameters["name"],
Price = (double)parameters["price"],
EnumValue = (MyEnum)parameters["enumValue"]
};
Data.TryAdd(product.Id, product);
return Created(product);
}

/// <summary>
/// Rates a product. This action targets a specific entity by id.
/// </summary>
/// <param name="key">The product id.</param>
/// <param name="parameters">The OData action parameters.</param>
[HttpPost]
[ResponseType(typeof(void))]
public IHttpActionResult Rate([FromODataUri] int key, ODataActionParameters parameters)
{
return StatusCode(HttpStatusCode.NoContent);
}

private static double GetRate(string state)
{
double taxRate;
Expand Down
242 changes: 242 additions & 0 deletions Swashbuckle.OData.Tests/Fixtures/ActionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
using System.Web.OData;
using System.Web.OData.Builder;
using System.Web.OData.Extensions;
using FluentAssertions;
using Microsoft.OData.Edm;
using Microsoft.Owin.Hosting;
using NUnit.Framework;
using Owin;
using Swashbuckle.Swagger;
using SwashbuckleODataSample.Models;

namespace Swashbuckle.OData.Tests
{
[TestFixture]
public class ActionTests
{
[Test]
public async Task It_supports_actions_with_only_body_paramters()
{
using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(SuppliersController))))
{
// Arrange
var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress);
// Verify that the OData route in the test controller is valid
var supplierDto = new SupplierDto
{
Name = "SupplierName",
Code = "SDTO",
Description = "SupplierDescription"
};
var result = await httpClient.PostAsJsonAsync("/odata/Suppliers/Default.Create", supplierDto);
result.IsSuccessStatusCode.Should().BeTrue();

// Act
var swaggerDocument = await httpClient.GetJsonAsync<SwaggerDocument>("swagger/docs/v1");

// Assert
PathItem pathItem;
swaggerDocument.paths.TryGetValue("/odata/Suppliers/Default.Create", out pathItem);
pathItem.Should().NotBeNull();
pathItem.post.Should().NotBeNull();
pathItem.post.parameters.Count.Should().Be(1);
pathItem.post.parameters.Single().@in.Should().Be("body");
pathItem.post.parameters.Single().schema.Should().NotBeNull();
pathItem.post.parameters.Single().schema.properties.Should().NotBeNull();
pathItem.post.parameters.Single().schema.properties.Count.Should().Be(3);
pathItem.post.parameters.Single().schema.properties.Should().ContainKey("code");
pathItem.post.parameters.Single().schema.properties.Should().ContainKey("name");
pathItem.post.parameters.Single().schema.properties.Single(pair => pair.Key == "name").Value.type.Should().Be("string");
pathItem.post.parameters.Single().schema.properties.Should().ContainKey("description");
pathItem.post.parameters.Single().schema.properties.Single(pair => pair.Key == "description").Value.type.Should().Be("string");
pathItem.post.parameters.Single().schema.required.Should().NotBeNull();
pathItem.post.parameters.Single().schema.required.Count.Should().Be(2);
pathItem.post.parameters.Single().schema.required.Should().Contain("code");
pathItem.post.parameters.Single().schema.required.Should().Contain("name");

await ValidationUtils.ValidateSwaggerJson();
}
}

[Test]
public async Task It_supports_actions_with_an_optional_enum_parameter()
{
using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(SuppliersController))))
{
// Arrange
var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress);
// Verify that the OData route in the test controller is valid
var supplierDto = new SupplierWithEnumDto
{
EnumValue = MyEnum.ValueOne
};
var result = await httpClient.PostAsJsonAsync("/odata/Suppliers/Default.CreateWithEnum", supplierDto);
result.IsSuccessStatusCode.Should().BeTrue();

// Act
var swaggerDocument = await httpClient.GetJsonAsync<SwaggerDocument>("swagger/docs/v1");

// Assert
PathItem pathItem;
swaggerDocument.paths.TryGetValue("/odata/Suppliers/Default.CreateWithEnum", out pathItem);
pathItem.Should().NotBeNull();
pathItem.post.Should().NotBeNull();
pathItem.post.parameters.Count.Should().Be(1);
pathItem.post.parameters.Single().@in.Should().Be("body");
pathItem.post.parameters.Single().schema.Should().NotBeNull();
pathItem.post.parameters.Single().schema.type.Should().Be("object");
pathItem.post.parameters.Single().schema.properties.Should().NotBeNull();
pathItem.post.parameters.Single().schema.properties.Count.Should().Be(1);
pathItem.post.parameters.Single().schema.properties.Should().ContainKey("EnumValue");
pathItem.post.parameters.Single().schema.properties.Single(pair => pair.Key == "EnumValue").Value.type.Should().Be("string");
pathItem.post.parameters.Single().schema.properties.Single(pair => pair.Key == "EnumValue").Value.@enum.Should().NotBeNull();
pathItem.post.parameters.Single().schema.properties.Single(pair => pair.Key == "EnumValue").Value.@enum.Count.Should().Be(2);
pathItem.post.parameters.Single().schema.properties.Single(pair => pair.Key == "EnumValue").Value.@enum.First().Should().Be(MyEnum.ValueOne.ToString());
pathItem.post.parameters.Single().schema.properties.Single(pair => pair.Key == "EnumValue").Value.@enum.Skip(1).First().Should().Be(MyEnum.ValueTwo.ToString());
pathItem.post.parameters.Single().schema.required.Should().BeNull();

await ValidationUtils.ValidateSwaggerJson();
}
}

[Test]
public async Task It_supports_actions_against_an_entity()
{
using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(SuppliersController))))
{
// Arrange
var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress);
// Verify that the OData route in the test controller is valid
var rating = new RatingDto
{
Rating = 1
};
var result = await httpClient.PostAsJsonAsync("/odata/Suppliers(1)/Default.Rate", rating);
result.IsSuccessStatusCode.Should().BeTrue();

// Act
var swaggerDocument = await httpClient.GetJsonAsync<SwaggerDocument>("swagger/docs/v1");

// Assert
PathItem pathItem;
swaggerDocument.paths.TryGetValue("/odata/Suppliers({Id})/Default.Rate", out pathItem);
pathItem.Should().NotBeNull();
pathItem.post.Should().NotBeNull();
pathItem.post.parameters.Count.Should().Be(2);

var idParameter = pathItem.post.parameters.SingleOrDefault(parameter => parameter.@in == "path");
idParameter.Should().NotBeNull();
idParameter.type.Should().Be("integer");
idParameter.format.Should().Be("int32");
idParameter.name.Should().Be("Id");

var bodyParameter = pathItem.post.parameters.SingleOrDefault(parameter => parameter.@in == "body");
bodyParameter.Should().NotBeNull();
bodyParameter.@in.Should().Be("body");
bodyParameter.schema.Should().NotBeNull();
bodyParameter.schema.type.Should().Be("object");
bodyParameter.schema.properties.Should().NotBeNull();
bodyParameter.schema.properties.Count.Should().Be(1);
bodyParameter.schema.properties.Should().ContainKey("Rating");
bodyParameter.schema.properties.Single(pair => pair.Key == "Rating").Value.type.Should().Be("integer");
bodyParameter.schema.properties.Single(pair => pair.Key == "Rating").Value.format.Should().Be("int32");
bodyParameter.schema.required.Should().NotBeNull();
bodyParameter.schema.required.Count.Should().Be(1);
bodyParameter.schema.required.Should().Contain("Rating");

await ValidationUtils.ValidateSwaggerJson();
}
}

private static void Configuration(IAppBuilder appBuilder, Type targetController)
{
var config = appBuilder.GetStandardHttpConfig(targetController);

// Define a route to a controller class that contains functions
config.MapODataServiceRoute("ODataRoute", "odata", GetEdmModel());

config.EnsureInitialized();
}

private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Supplier>("Suppliers");
var entityType = builder.EntityType<Supplier>();

var create = entityType.Collection.Action("Create");
create.ReturnsFromEntitySet<Supplier>("Suppliers");
create.Parameter<string>("code").OptionalParameter = false;
create.Parameter<string>("name").OptionalParameter = false;
create.Parameter<string>("description");

var createWithEnum = entityType.Collection.Action("CreateWithEnum");
createWithEnum.ReturnsFromEntitySet<Supplier>("Suppliers");
createWithEnum.Parameter<MyEnum?>("EnumValue");

entityType.Action("Rate")
.Parameter<int>("Rating");

return builder.GetEdmModel();
}
}

public class SupplierDto
{
public string Code { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}

public class SupplierWithEnumDto
{
public MyEnum EnumValue { get; set; }
}

public class RatingDto
{
public int Rating { get; set; }
}

public class Supplier
{
[Key]
public long Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}

public class SuppliersController : ODataController
{
[HttpPost]
[ResponseType(typeof(Supplier))]
public IHttpActionResult Create(ODataActionParameters parameters)
{
return Created(new Supplier {Id = 1});
}

[HttpPost]
[ResponseType(typeof(Supplier))]
public IHttpActionResult CreateWithEnum(ODataActionParameters parameters)
{
return Created(new Supplier { Id = 1 });
}

[HttpPost]
public IHttpActionResult Rate([FromODataUri] int key, ODataActionParameters parameters)
{
parameters.Should().ContainKey("Rating");

return StatusCode(HttpStatusCode.NoContent);
}
}
}
1 change: 1 addition & 0 deletions Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
<Compile Include="Fixtures\FunctionTests.cs" />
<Compile Include="Fixtures\GetHttpMethodNameTests.cs" />
<Compile Include="Fixtures\ODataSwaggerProviderTests.cs" />
<Compile Include="Fixtures\ActionTests.cs" />
<Compile Include="Fixtures\ParameterTests\ByteParameterAndResponseTests.cs" />
<Compile Include="Fixtures\PostTests.cs" />
<Compile Include="Fixtures\PatchTests.cs" />
Expand Down
1 change: 1 addition & 0 deletions Swashbuckle.OData/DefaultCompositionRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ private static IEnumerable<IParameterMapper> GetParameterMappers()
{
return new List<IParameterMapper>
{
new MapToODataActionParameter(),
new MapRestierParameter(),
new MapByParameterName(),
new MapByDescription(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
using System.Diagnostics.Contracts;
using System.Web.Http.Controllers;
using System.Web.OData;

namespace Swashbuckle.OData.Descriptions
{
internal static class HttpParameterDescriptorExtensions
{
public static bool IsODataQueryOptions(this HttpParameterDescriptor parameterDescriptor)
public static bool IsODataLibraryType(this HttpParameterDescriptor parameterDescriptor)
{
Contract.Requires(parameterDescriptor != null);

return IsODataODataActionParameters(parameterDescriptor) || IsODataQueryOptions(parameterDescriptor);
}

private static bool IsODataODataActionParameters(this HttpParameterDescriptor parameterDescriptor)
{
Contract.Requires(parameterDescriptor != null);

var parameterType = parameterDescriptor.ParameterType;
Contract.Assume(parameterType != null);

return parameterType == typeof(ODataActionParameters);
}

private static bool IsODataQueryOptions(this HttpParameterDescriptor parameterDescriptor)
{
Contract.Requires(parameterDescriptor != null);

Expand Down
2 changes: 1 addition & 1 deletion Swashbuckle.OData/Descriptions/MapByDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public HttpParameterDescriptor Map(Parameter swaggerParameter, int parameterInde
if (swaggerParameter.description != null && swaggerParameter.description.StartsWith("key:"))
{
var parameterDescriptor = actionDescriptor.GetParameters()?.SingleOrDefault(descriptor => descriptor.ParameterName == "key");
if (parameterDescriptor != null && !parameterDescriptor.IsODataQueryOptions())
if (parameterDescriptor != null && !parameterDescriptor.IsODataLibraryType())
{
var httpControllerDescriptor = actionDescriptor.ControllerDescriptor;
Contract.Assume(httpControllerDescriptor != null);
Expand Down
2 changes: 1 addition & 1 deletion Swashbuckle.OData/Descriptions/MapByIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public HttpParameterDescriptor Map(Parameter swaggerParameter, int parameterInde
if (swaggerParameter.@in != "query" && parameterIndex < actionDescriptor.GetParameters().Count)
{
var parameterDescriptor = actionDescriptor.GetParameters()[parameterIndex];
if (parameterDescriptor != null && !parameterDescriptor.IsODataQueryOptions())
if (parameterDescriptor != null && !parameterDescriptor.IsODataLibraryType())
{
var httpControllerDescriptor = actionDescriptor.ControllerDescriptor;
Contract.Assume(httpControllerDescriptor != null);
Expand Down
Loading

0 comments on commit d355430

Please sign in to comment.