Skip to content

Commit 9b1f50b

Browse files
Add examples for using 'some' OData with OpenAPI
1 parent 1987182 commit 9b1f50b

File tree

15 files changed

+752
-0
lines changed

15 files changed

+752
-0
lines changed

asp.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Common.OData.ApiExplorer.Te
186186
EndProject
187187
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalOpenApiExample", "examples\AspNetCore\WebApi\MinimalOpenApiExample\MinimalOpenApiExample.csproj", "{124C18D1-F72A-4380-AE40-E7511AC16C62}"
188188
EndProject
189+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SomeODataOpenApiExample", "examples\AspNetCore\OData\SomeODataOpenApiExample\SomeODataOpenApiExample.csproj", "{94A9AF81-A7BE-4E6C-81B1-8BFF4B6E1B78}"
190+
EndProject
191+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SomeOpenApiODataWebApiExample", "examples\AspNet\OData\SomeOpenApiODataWebApiExample\SomeOpenApiODataWebApiExample.csproj", "{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}"
192+
EndProject
189193
Global
190194
GlobalSection(SharedMSBuildProjectFiles) = preSolution
191195
src\Common\test\Common.Acceptance.Tests\Common.Acceptance.Tests.projitems*{0be9efaa-3627-46fe-9861-9121ee8f0e26}*SharedItemsImports = 5
@@ -405,6 +409,14 @@ Global
405409
{124C18D1-F72A-4380-AE40-E7511AC16C62}.Debug|Any CPU.Build.0 = Debug|Any CPU
406410
{124C18D1-F72A-4380-AE40-E7511AC16C62}.Release|Any CPU.ActiveCfg = Release|Any CPU
407411
{124C18D1-F72A-4380-AE40-E7511AC16C62}.Release|Any CPU.Build.0 = Release|Any CPU
412+
{94A9AF81-A7BE-4E6C-81B1-8BFF4B6E1B78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
413+
{94A9AF81-A7BE-4E6C-81B1-8BFF4B6E1B78}.Debug|Any CPU.Build.0 = Debug|Any CPU
414+
{94A9AF81-A7BE-4E6C-81B1-8BFF4B6E1B78}.Release|Any CPU.ActiveCfg = Release|Any CPU
415+
{94A9AF81-A7BE-4E6C-81B1-8BFF4B6E1B78}.Release|Any CPU.Build.0 = Release|Any CPU
416+
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
417+
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
418+
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
419+
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5}.Release|Any CPU.Build.0 = Release|Any CPU
408420
EndGlobalSection
409421
GlobalSection(SolutionProperties) = preSolution
410422
HideSolutionNode = FALSE
@@ -487,6 +499,8 @@ Global
487499
{B39C3FE5-227F-4403-B246-1277906ACF7D} = {49EA6476-901C-4D4F-8E45-98BC8A2780EB}
488500
{496A5B79-AFD2-45AC-AF9A-1CD28A7E1CDB} = {031927C1-BF12-42A9-A91D-6907E8C7F1C7}
489501
{124C18D1-F72A-4380-AE40-E7511AC16C62} = {E0E64F6F-FB0C-4534-B815-2217700B50BA}
502+
{94A9AF81-A7BE-4E6C-81B1-8BFF4B6E1B78} = {49EA6476-901C-4D4F-8E45-98BC8A2780EB}
503+
{AC952FBF-D7DC-4DE4-AD1C-4CEA589034F5} = {7BB01633-6E2C-4837-B618-C7F09B18E99E}
490504
EndGlobalSection
491505
GlobalSection(ExtensibilityGlobals) = postSolution
492506
SolutionGuid = {91FE116A-CEFB-4304-A8A6-CFF021C7453A}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace ApiVersioning.Examples;
2+
3+
/// <summary>
4+
/// Represents a book.
5+
/// </summary>
6+
public class Book
7+
{
8+
/// <summary>
9+
/// Gets or sets the book identifier.
10+
/// </summary>
11+
/// <value>The International Standard Book Number (ISBN).</value>
12+
public string Id { get; set; }
13+
14+
/// <summary>
15+
/// Gets or sets the book author.
16+
/// </summary>
17+
/// <value>The author of the book.</value>
18+
public string Author { get; set; }
19+
20+
/// <summary>
21+
/// Gets or sets the book title.
22+
/// </summary>
23+
/// <value>The title of the book.</value>
24+
public string Title { get; set; }
25+
26+
/// <summary>
27+
/// Gets or sets the book publication year.
28+
/// </summary>
29+
/// <value>The year the book was first published.</value>
30+
public int Published { get; set; }
31+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
namespace ApiVersioning.Examples;
2+
3+
using Asp.Versioning;
4+
using Microsoft.AspNet.OData.Query;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Web.Http;
8+
using System.Web.Http.Description;
9+
10+
/// <summary>
11+
/// Represents a RESTful service of books.
12+
/// </summary>
13+
[ApiVersion( 1.0 )]
14+
[RoutePrefix( "api/books" )]
15+
public class BooksController : ApiController
16+
{
17+
private static readonly Book[] books = new Book[]
18+
{
19+
new() { Id = "9781847490599", Title = "Anna Karenina", Author = "Leo Tolstoy", Published = 1878 },
20+
new() { Id = "9780198800545", Title = "War and Peace", Author = "Leo Tolstoy", Published = 1869 },
21+
new() { Id = "9780684801520", Title = "The Great Gatsby", Author = "F. Scott Fitzgerald", Published = 1925 },
22+
new() { Id = "9780486280615", Title = "The Adventures of Huckleberry Finn", Author = "Mark Twain", Published = 1884 },
23+
new() { Id = "9780140430820", Title = "Moby Dick", Author = "Herman Melville", Published = 1851 },
24+
new() { Id = "9780060934347", Title = "Don Quixote", Author = "Miguel de Cervantes", Published = 1605 },
25+
};
26+
27+
/// <summary>
28+
/// Gets all books.
29+
/// </summary>
30+
/// <param name="options">The current OData query options.</param>
31+
/// <returns>All available books.</returns>
32+
/// <response code="200">The successfully retrieved books.</response>
33+
[HttpGet]
34+
[Route]
35+
[ResponseType( typeof( IEnumerable<Book> ) )]
36+
public IHttpActionResult Get( ODataQueryOptions<Book> options ) =>
37+
Ok( options.ApplyTo( books.AsQueryable() ) );
38+
39+
/// <summary>
40+
/// Gets a single book.
41+
/// </summary>
42+
/// <param name="id">The requested book identifier.</param>
43+
/// <param name="options">The current OData query options.</param>
44+
/// <returns>The requested book.</returns>
45+
/// <response code="200">The book was successfully retrieved.</response>
46+
/// <response code="404">The book does not exist.</response>
47+
[HttpGet]
48+
[Route( "{id}" )]
49+
[ResponseType( typeof( Book ) )]
50+
public IHttpActionResult Get( string id, ODataQueryOptions<Book> options )
51+
{
52+
var book = books.FirstOrDefault( book => book.Id == id );
53+
54+
if ( book == null )
55+
{
56+
return NotFound();
57+
}
58+
59+
return Ok( options.ApplyTo( book, new ODataQuerySettings(), default ) );
60+
}
61+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace ApiVersioning.Examples;
2+
3+
using Microsoft.Owin.Hosting;
4+
using System.Diagnostics;
5+
6+
/// <summary>
7+
/// Represents the current application.
8+
/// </summary>
9+
public class Program
10+
{
11+
private const string Url = "http://localhost:9008/";
12+
private const string LaunchUrl = Url + "swagger";
13+
private static readonly ManualResetEvent resetEvent = new( false );
14+
15+
/// <summary>
16+
/// The main entry point to the application.
17+
/// </summary>
18+
/// <param name="args">The arguments provided at start-up, if any.</param>
19+
public static void Main( string[] args )
20+
{
21+
Console.CancelKeyPress += OnCancel;
22+
23+
using ( WebApp.Start<Startup>( Url ) )
24+
{
25+
Console.WriteLine( "Content root path: " + Startup.ContentRootPath );
26+
Console.WriteLine( "Now listening on: " + Url );
27+
Console.WriteLine( "Application started. Press Ctrl+C to shut down." );
28+
Process.Start( LaunchUrl );
29+
resetEvent.WaitOne();
30+
}
31+
32+
Console.CancelKeyPress -= OnCancel;
33+
}
34+
35+
private static void OnCancel( object sender, ConsoleCancelEventArgs e )
36+
{
37+
Console.Write( "Application is shutting down..." );
38+
e.Cancel = true;
39+
resetEvent.Set();
40+
}
41+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net48</TargetFramework>
5+
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(MSBuildThisFileName).xml</DocumentationFile>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<Reference Include="System.ComponentModel.DataAnnotations" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Swashbuckle.Core" Version="5.6.0" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\..\..\..\src\AspNet\OData\src\Asp.Versioning.WebApi.OData.ApiExplorer\Asp.Versioning.WebApi.OData.ApiExplorer.csproj" />
18+
</ItemGroup>
19+
20+
</Project>
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
namespace ApiVersioning.Examples;
2+
3+
using Asp.Versioning;
4+
using Asp.Versioning.Conventions;
5+
using Microsoft.AspNet.OData.Extensions;
6+
using Microsoft.OData;
7+
using Newtonsoft.Json.Serialization;
8+
using Owin;
9+
using Swashbuckle.Application;
10+
using System.IO;
11+
using System.Reflection;
12+
using System.Text;
13+
using System.Web.Http;
14+
using System.Web.Http.Description;
15+
using static Microsoft.AspNet.OData.Query.AllowedQueryOptions;
16+
17+
/// <summary>
18+
/// Represents the startup process for the application.
19+
/// </summary>
20+
public class Startup
21+
{
22+
/// <summary>
23+
/// Configures the application using the provided builder.
24+
/// </summary>
25+
/// <param name="builder">The current application builder.</param>
26+
public void Configuration( IAppBuilder builder )
27+
{
28+
var configuration = new HttpConfiguration();
29+
var httpServer = new HttpServer( configuration );
30+
31+
// note: this example application intentionally only illustrates the
32+
// bare minimum configuration for OpenAPI with partial OData support.
33+
// see the OpenAPI or OData OpenAPI examples for additional options.
34+
35+
configuration.EnableDependencyInjection();
36+
configuration.Select();
37+
configuration.AddApiVersioning();
38+
39+
// note: this is required to make the default swagger json
40+
// settings match the odata conventions applied by EnableLowerCamelCase()
41+
configuration.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
42+
new CamelCasePropertyNamesContractResolver();
43+
44+
// NOTE: when you mix OData and non-Data controllers in Web API, it's RECOMMENDED to only use
45+
// convention-based routing. using attribute routing may not work as expected due to limitations
46+
// in the underlying routing system. the order of route registration is important as well.
47+
//
48+
// for example:
49+
//
50+
// configuration.MapVersionedODataRoute( "odata", "api", modelBuilder );
51+
// configuration.Routes.MapHttpRoute( "Default", "api/{controller}/{id}", new { id = RouteParameter.Optional } );
52+
//
53+
// for more information see the advanced OData example
54+
configuration.MapHttpAttributeRoutes();
55+
56+
// add the versioned IApiExplorer and capture the strongly-typed implementation (e.g. ODataApiExplorer vs IApiExplorer)
57+
// note: the specified format code will format the version as "'v'major[.minor][-status]"
58+
var apiExplorer = configuration.AddODataApiExplorer(
59+
options =>
60+
{
61+
// add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
62+
// note: the specified format code will format the version as "'v'major[.minor][-status]"
63+
options.GroupNameFormat = "'v'VVV";
64+
65+
// configure query options (which cannot otherwise be configured by OData conventions)
66+
options.QueryOptions.Controller<BooksController>()
67+
.Action( c => c.Get( default ) )
68+
.Allow( Skip | Count )
69+
.AllowTop( 100 )
70+
.AllowOrderBy( "title", "published" );
71+
} );
72+
73+
configuration.EnableSwagger(
74+
"{apiVersion}/swagger",
75+
swagger =>
76+
{
77+
// build a swagger document and endpoint for each discovered API version
78+
swagger.MultipleApiVersions(
79+
( apiDescription, version ) => apiDescription.GetGroupName() == version,
80+
info =>
81+
{
82+
foreach ( var group in apiExplorer.ApiDescriptions )
83+
{
84+
var description = new StringBuilder( "A sample application with some OData, OpenAPI, Swashbuckle, and API versioning." );
85+
86+
if ( group.IsDeprecated )
87+
{
88+
description.Append( " This API version has been deprecated." );
89+
}
90+
91+
if ( group.SunsetPolicy is SunsetPolicy policy )
92+
{
93+
if ( policy.Date is DateTimeOffset when )
94+
{
95+
description.Append( " The API will be sunset on " )
96+
.Append( when.Date.ToShortDateString() )
97+
.Append( '.' );
98+
}
99+
100+
if ( policy.HasLinks )
101+
{
102+
description.AppendLine();
103+
104+
for ( var i = 0; i < policy.Links.Count; i++ )
105+
{
106+
var link = policy.Links[i];
107+
108+
if ( link.Type == "text/html" )
109+
{
110+
description.AppendLine();
111+
112+
if ( link.Title.HasValue )
113+
{
114+
description.Append( link.Title.Value ).Append( ": " );
115+
}
116+
117+
description.Append( link.LinkTarget.OriginalString );
118+
}
119+
}
120+
}
121+
}
122+
123+
info.Version( group.Name, $"Sample API {group.ApiVersion}" )
124+
.Contact( c => c.Name( "Bill Mei" ).Email( "bill.mei@somewhere.com" ) )
125+
.Description( description.ToString() )
126+
.License( l => l.Name( "MIT" ).Url( "https://opensource.org/licenses/MIT" ) )
127+
.TermsOfService( "Shareware" );
128+
}
129+
} );
130+
131+
// add a custom operation filter which documents the implicit API version parameter
132+
swagger.OperationFilter<SwaggerDefaultValues>();
133+
134+
// integrate xml comments
135+
swagger.IncludeXmlComments( XmlCommentsFilePath );
136+
} )
137+
.EnableSwaggerUi( swagger => swagger.EnableDiscoveryUrlSelector() );
138+
139+
builder.UseWebApi( httpServer );
140+
}
141+
142+
/// <summary>
143+
/// Get the root content path.
144+
/// </summary>
145+
/// <value>The root content path of the application.</value>
146+
public static string ContentRootPath
147+
{
148+
get
149+
{
150+
var app = AppDomain.CurrentDomain;
151+
152+
if ( string.IsNullOrEmpty( app.RelativeSearchPath ) )
153+
{
154+
return app.BaseDirectory;
155+
}
156+
157+
return app.RelativeSearchPath;
158+
}
159+
}
160+
161+
private static string XmlCommentsFilePath
162+
{
163+
get
164+
{
165+
var fileName = typeof( Startup ).GetTypeInfo().Assembly.GetName().Name + ".xml";
166+
return Path.Combine( ContentRootPath, fileName );
167+
}
168+
}
169+
}

0 commit comments

Comments
 (0)