Skip to content

Commit

Permalink
Merge pull request #26 from rbeauchamp/feature/21
Browse files Browse the repository at this point in the history
Provide support for custom routes
  • Loading branch information
rbeauchamp committed Dec 19, 2015
2 parents e3aebae + 1527b4f commit 69c4bc7
Show file tree
Hide file tree
Showing 67 changed files with 2,118 additions and 666 deletions.
54 changes: 24 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,30 @@ In `SwaggerConfig` configure the custom provider:
c.CustomProvider(defaultProvider => new ODataSwaggerProvider(defaultProvider, c));
```

### Custom Routes ###

The following snippet demonstrates how to configure a custom OData route such that it will appear in the Swagger UI:
```csharp
// Let's say you map a custom OData route that doesn't follow the typical conventions
var customODataRoute = config.MapODataServiceRoute("CustomODataRoute", ODataRoutePrefix, GetModel(), batchHandler: null, pathHandler: new DefaultODataPathHandler(), routingConventions: myCustomConventions);

// Then describe your route to Swashbuckle.OData so that it will appear in the Swagger UI
config.AddCustomSwaggerRoute(customODataRoute, "/Customers({Id})/Orders")
.Operation(HttpMethod.Post)
// The name of the parameter as it appears in the path
.PathParameter<int>("Id")
// The name of the parameter as it appears in the controller action
.BodyParameter<Order>("order");
```
The above route resolves to an `OrderController` action of:
```csharp
[ResponseType(typeof(Order))]
public async Task<IHttpActionResult> Post([FromODataUri] int customerId, Order order)
{
...
}
```

### OWIN ###

If your service is hosted using OWIN middleware, configure the custom provider as follows:
Expand All @@ -42,33 +66,3 @@ httpConfiguration
})
.EnableSwaggerUi();
```

### Upgrading to Swashbuckle.OData v2 ###

To simplify configuration, this version of Swashbuckle.OData leverages .NET 4.5.2. Previous versions were compiled against .NET 4.0.

Also, if upgrading from v1.0, revert the previously recommended `SwaggerConfig` changes:

Revert `SwaggerConfig` to the original `Swashbuckle`-supplied version:
```csharp
[assembly: PreApplicationStartMethod(typeof(SwaggerConfig), "Register")]

namespace Swashbuckle.OData
{
public class SwaggerConfig
{
public static void Register()
{
```

Remove the call to `SwaggerConfig.Register(edmModel);`:
```csharp
public static void Register(HttpConfiguration config)
{
var builder = new ODataConventionModelBuilder();
var edmModel = builder.GetEdmModel();
config.MapODataServiceRoute("odata", "odata", edmModel);

//SwaggerConfig.Register(edmModel);
}
```
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>Supports both WebApi and OData endpoints. Groups RESTier endpoints by EntitySet name. Bug fixes. </ReleaseNotes>
<ReleaseNotes>Supports custom OData routes, Refactored to support DI per http://blog.ploeh.dk/2014/05/19/di-friendly-library/</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.5.2</Version>
<Version>2.6.0</Version>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Swashbuckle.OData\Swashbuckle.OData.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Linq;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.OData.Routing;
using System.Web.OData.Routing.Conventions;
using SwashbuckleODataSample.ODataControllers;

namespace SwashbuckleODataSample
{
public class CustomNavigationPropertyRoutingConvention : EntitySetRoutingConvention
{
public override string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap)
{
var controllerType = controllerContext.Controller.GetType();

if (typeof (CustomersController) == controllerType)
{
}
else if (typeof (OrdersController) == controllerType)
{
if (odataPath.PathTemplate.Equals("~/entityset/key/navigation")) //POST OR GET
{
controllerContext.RouteData.Values["customerID"] = (odataPath.Segments[1] as KeyValuePathSegment).Value;
return controllerContext.Request.Method.ToString();
}
if (odataPath.PathTemplate.Equals("~/entityset/key/navigation/key")) //PATCH OR DELETE
{
controllerContext.RouteData.Values["customerID"] = (odataPath.Segments[1] as KeyValuePathSegment).Value;

controllerContext.RouteData.Values["key"] = (odataPath.Segments[3] as KeyValuePathSegment).Value;
return controllerContext.Request.Method.ToString();
}
}

return base.SelectAction(odataPath, controllerContext, actionMap);
}

public override string SelectController(ODataPath odataPath, HttpRequestMessage request)
{
// We use always use the last naviation as the controller vs. the initial entityset
if (odataPath.PathTemplate.Contains("~/entityset/key/navigation"))
{
// Find controller. Controller should be last navigation property
return ODataSegmentKinds.Navigation == odataPath.Segments[odataPath.Segments.Count - 1].SegmentKind
? odataPath.Segments[odataPath.Segments.Count - 1].ToString()
: odataPath.Segments[odataPath.Segments.Count - 2].ToString();
}
return base.SelectController(odataPath, request);
}
}
}
20 changes: 18 additions & 2 deletions Swashbuckle.OData.Sample/App_Start/ODataConfig.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
using System.Web.Http;
using System.Collections.Generic;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Description;
using System.Web.Http.Dispatcher;
using System.Web.OData.Builder;
using System.Web.OData.Extensions;
using System.Web.OData.Routing;
using System.Web.OData.Routing.Conventions;
using Microsoft.OData.Edm;
using Microsoft.Restier.EntityFramework;
using Microsoft.Restier.WebApi;
using Microsoft.Restier.WebApi.Batch;
using Swashbuckle.OData;
using Swashbuckle.OData.Descriptions;
using SwashbuckleODataSample.Models;
using SwashbuckleODataSample.Repositories;
using SwashbuckleODataSample.Versioning;
Expand Down Expand Up @@ -40,7 +47,16 @@ private static void ConfigureWebApiOData(HttpConfiguration config)
config.MapODataServiceRoute("odata/v2", "odata/v2", GetFakeModel());
controllerSelector.RouteVersionSuffixMapping.Add("odata/v2", "V2");

// Define a default non-versioned route
// Define a custom route with custom routing conventions
var conventions = ODataRoutingConventions.CreateDefault();
conventions.Insert(0, new CustomNavigationPropertyRoutingConvention());
var customODataRoute = config.MapODataServiceRoute("CustomODataRoute", ODataRoutePrefix, GetModel(), batchHandler: null, pathHandler: new DefaultODataPathHandler(), routingConventions: conventions);
config.AddCustomSwaggerRoute(customODataRoute, "/Customers({Id})/Orders")
.Operation(HttpMethod.Post)
.PathParameter<int>("Id")
.BodyParameter<Order>("order");

// Define a default non-versioned route (default route should be at the end as a last catch-all)
config.MapODataServiceRoute("DefaultODataRoute", ODataRoutePrefix, GetModel());
}

Expand Down
22 changes: 22 additions & 0 deletions Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,28 @@ public IQueryable<Order> GetOrders()
return _db.Orders;
}

/// <summary>
/// An example of a custom route. Create a new order for the customer with the given id
/// </summary>
/// <param name="customerId">The customer id</param>
/// <param name="order">Order details</param>
[ResponseType(typeof(Order))]
public async Task<IHttpActionResult> Post([FromODataUri] int customerId, Order order)
{
order.OrderId = SequentialGuidGenerator.Generate(SequentialGuidType.SequentialAtEnd);
order.CustomerId = customerId;

if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

_db.Orders.Add(order);
await _db.SaveChangesAsync();

return Created(order);
}

/// <summary>
/// Query the order by id
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
<Compile Include="ApiControllers\Client.cs" />
<Compile Include="ApiControllers\ClientsController.cs" />
<Compile Include="ApiControllers\Project.cs" />
<Compile Include="App_Start\CustomNavigationPropertyRoutingConvention.cs" />
<Compile Include="App_Start\FormatterConfig.cs" />
<Compile Include="App_Start\SwaggerConfig.cs" />
<Compile Include="App_Start\ODataConfig.cs" />
Expand Down
2 changes: 1 addition & 1 deletion Swashbuckle.OData.Sample/Utils/SequentialGuidGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace SwashbuckleODataSample.Utils
{
/// <summary>
/// Generates <see cref="System.Guid" /> values using strategy from Jeremy Todd.
/// <see cref="http://www.codeproject.com/Articles/388157/GUIDs-as-fast-primary-keys-under-multiple-database" />
/// See <a href="http://www.codeproject.com/Articles/388157/GUIDs-as-fast-primary-keys-under-multiple-database">GUIDs as fast primary keys under multiple databases</a>.
/// </summary>
public static class SequentialGuidGenerator
{
Expand Down
33 changes: 33 additions & 0 deletions Swashbuckle.OData.Tests/Fixtures/CustomRouteTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Owin.Hosting;
using NUnit.Framework;
using Swashbuckle.OData.Tests.WebHost;
using Swashbuckle.Swagger;
using SwashbuckleODataSample;

namespace Swashbuckle.OData.Tests
{
[TestFixture]
public class CustomRouteTests
{
[Test]
public async Task It_allows_definition_of_custom_routes()
{
using (WebApp.Start(TestWebApiStartup.BaseAddress, appBuilder => new TestWebApiStartup().Configuration(appBuilder)))
{
// Arrange
var httpClient = HttpClientUtils.GetHttpClient(TestWebApiStartup.BaseAddress, ODataConfig.ODataRoutePrefix);

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

// Assert
PathItem pathItem;
swaggerDocument.paths.TryGetValue("/odata/Customers({Id})/Orders", out pathItem);
pathItem.Should().NotBeNull();
pathItem.post.Should().NotBeNull();
}
}
}
}
3 changes: 2 additions & 1 deletion Swashbuckle.OData.Tests/Fixtures/RestierTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ public async Task It_supports_entities_with_multiple_keys()

// Assert
PathItem pathItem;
swaggerDocument.paths.TryGetValue("/restier/Order_Details(OrderID={OrderID}, ProductID={ProductID})", out pathItem);
swaggerDocument.paths.TryGetValue("/restier/OrderDetails(OrderId={OrderId}, ProductId={ProductId})", out pathItem);
pathItem.Should().NotBeNull();
var getResponse = pathItem.get.responses.SingleOrDefault(response => response.Key == "200");
getResponse.Should().NotBeNull();
}
Expand Down
2 changes: 1 addition & 1 deletion Swashbuckle.OData.Tests/NorthwindAPI/Category.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public Category()
Products = new HashSet<Product>();
}

public int CategoryID { get; set; }
public int CategoryId { get; set; }

[Required]
[StringLength(15)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public CustomerDemographic()

[Key]
[StringLength(10)]
public string CustomerTypeID { get; set; }
public string CustomerTypeId { get; set; }

[Column(TypeName = "ntext")]
public string CustomerDesc { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion Swashbuckle.OData.Tests/NorthwindAPI/Employee.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public Employee()
Territories = new HashSet<Territory>();
}

public int EmployeeID { get; set; }
public int EmployeeId { get; set; }

[Required]
[StringLength(20)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void Configuration(IAppBuilder appBuilder, Action<SwaggerDocsConfig> unit
// Wrap the default SwaggerGenerator with additional behavior (e.g. caching) or provide an
// alternative implementation for ISwaggerProvider with the CustomProvider option.
//
c.CustomProvider(defaultProvider => new ODataSwaggerProvider(defaultProvider, c, () => httpConfiguration));
c.CustomProvider(defaultProvider => new ODataSwaggerProvider(defaultProvider, c, httpConfiguration));
unitTestConfigs?.Invoke(c);
})
Expand Down
14 changes: 7 additions & 7 deletions Swashbuckle.OData.Tests/NorthwindAPI/NorthwindContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public NorthwindContext() : base("name=NorthwindContext")
public virtual DbSet<CustomerDemographic> CustomerDemographics { get; set; }
public virtual DbSet<NorthwindCustomer> Customers { get; set; }
public virtual DbSet<Employee> Employees { get; set; }
public virtual DbSet<Order_Detail> Order_Details { get; set; }
public virtual DbSet<OrderDetail> OrderDetails { get; set; }
public virtual DbSet<NorthwindOrder> Orders { get; set; }
public virtual DbSet<Product> Products { get; set; }
public virtual DbSet<Region> Regions { get; set; }
Expand All @@ -27,27 +27,27 @@ public NorthwindContext() : base("name=NorthwindContext")

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<CustomerDemographic>().Property(e => e.CustomerTypeID).IsFixedLength();
modelBuilder.Entity<CustomerDemographic>().Property(e => e.CustomerTypeId).IsFixedLength();

modelBuilder.Entity<CustomerDemographic>().HasMany(e => e.Customers).WithMany(e => e.CustomerDemographics).Map(m => m.ToTable("CustomerCustomerDemo").MapLeftKey("CustomerTypeID").MapRightKey("CustomerID"));

modelBuilder.Entity<NorthwindCustomer>().Property(e => e.CustomerID).IsFixedLength();
modelBuilder.Entity<NorthwindCustomer>().Property(e => e.CustomerId).IsFixedLength();

modelBuilder.Entity<Employee>().HasMany(e => e.Employees1).WithOptional(e => e.Employee1).HasForeignKey(e => e.ReportsTo);

modelBuilder.Entity<Employee>().HasMany(e => e.Territories).WithMany(e => e.Employees).Map(m => m.ToTable("EmployeeTerritories").MapLeftKey("EmployeeID").MapRightKey("TerritoryID"));

modelBuilder.Entity<Order_Detail>().Property(e => e.UnitPrice).HasPrecision(19, 4);
modelBuilder.Entity<OrderDetail>().Property(e => e.UnitPrice).HasPrecision(19, 4);

modelBuilder.Entity<NorthwindOrder>().Property(e => e.CustomerID).IsFixedLength();
modelBuilder.Entity<NorthwindOrder>().Property(e => e.CustomerId).IsFixedLength();

modelBuilder.Entity<NorthwindOrder>().Property(e => e.Freight).HasPrecision(19, 4);

modelBuilder.Entity<NorthwindOrder>().HasMany(e => e.Order_Details).WithRequired(e => e.NorthwindOrder).WillCascadeOnDelete(false);
modelBuilder.Entity<NorthwindOrder>().HasMany(e => e.OrderDetails).WithRequired(e => e.NorthwindOrder).WillCascadeOnDelete(false);

modelBuilder.Entity<Product>().Property(e => e.UnitPrice).HasPrecision(19, 4);

modelBuilder.Entity<Product>().HasMany(e => e.Order_Details).WithRequired(e => e.Product).WillCascadeOnDelete(false);
modelBuilder.Entity<Product>().HasMany(e => e.OrderDetails).WithRequired(e => e.Product).WillCascadeOnDelete(false);

modelBuilder.Entity<Region>().Property(e => e.RegionDescription).IsFixedLength();

Expand Down
2 changes: 1 addition & 1 deletion Swashbuckle.OData.Tests/NorthwindAPI/NorthwindCustomer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public NorthwindCustomer()

[Key]
[StringLength(5)]
public string CustomerID { get; set; }
public string CustomerId { get; set; }

[Required]
[StringLength(40)]
Expand Down
10 changes: 5 additions & 5 deletions Swashbuckle.OData.Tests/NorthwindAPI/NorthwindOrder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ public class NorthwindOrder
[SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public NorthwindOrder()
{
Order_Details = new HashSet<Order_Detail>();
OrderDetails = new HashSet<OrderDetail>();
}

[Key]
public int OrderID { get; set; }
public int OrderId { get; set; }

[StringLength(5)]
public string CustomerID { get; set; }
public string CustomerId { get; set; }

public int? EmployeeID { get; set; }
public int? EmployeeId { get; set; }

public DateTime? OrderDate { get; set; }

Expand Down Expand Up @@ -56,7 +56,7 @@ public NorthwindOrder()
public virtual Employee Employee { get; set; }

[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<Order_Detail> Order_Details { get; set; }
public virtual ICollection<OrderDetail> OrderDetails { get; set; }

public virtual Shipper Shipper { get; set; }
}
Expand Down
Loading

0 comments on commit 69c4bc7

Please sign in to comment.