diff --git a/License.txt b/License.txt index 6a4d2a5..b6a9e37 100644 --- a/License.txt +++ b/License.txt @@ -19,3 +19,19 @@ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPO NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------------------------------------------------------------------ + +Certain portions of this code is Copyright (c) 2013, Richard Morris - All rights reserved. + +For those portions, the following license applies: + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHA \ No newline at end of file diff --git a/README.md b/README.md index da096ae..8112710 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@ Swashbuckle.OData Extends Swashbuckle with WebApi OData v4 support! -Implements a custom Swagger Provider that converts an Entity Data Model to a Swagger Document. - ### Try it out! ### ## Getting Started ## diff --git a/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj b/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj index d486237..5b5ae16 100644 --- a/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj +++ b/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj @@ -25,12 +25,12 @@ Richard Beauchamp Extends Swashbuckle with WebApi OData v4 support! Extends Swashbuckle with WebApi OData v4 support! - Simplified configuration, upgraded to .NET 4.5, fixed metadata path in Swagger UI, uses correct OData route prefix + Now supports customization of generated swagger docs via SwaggerConfig, Verifies entity data model against the OData API, Customize output with ApiExplorer-supported attributes https://github.com/rbeauchamp/Swashbuckle.OData https://github.com/rbeauchamp/Swashbuckle.OData/blob/master/License.txt Copyright 2015 Swashbuckle Swagger SwaggerUi OData Documentation Discovery Help WebApi AspNet AspNetWebApi Docs WebHost IIS - 2.0.0 + 2.1.0 diff --git a/Swashbuckle.OData.Tests/Customer.cs b/Swashbuckle.OData.Sample/ApiControllers/Client.cs similarity index 53% rename from Swashbuckle.OData.Tests/Customer.cs rename to Swashbuckle.OData.Sample/ApiControllers/Client.cs index 54a36bc..ca9780c 100644 --- a/Swashbuckle.OData.Tests/Customer.cs +++ b/Swashbuckle.OData.Sample/ApiControllers/Client.cs @@ -1,13 +1,13 @@ using System.Collections.Generic; -namespace Swashbuckle.OData.Tests +namespace SwashbuckleODataSample.Models { - public class Customer + public class Client { public int Id { get; set; } public string Name { get; set; } - public IList Orders { get; set; } + public IList Projects { get; set; } } } \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/ApiControllers/ClientsController.cs b/Swashbuckle.OData.Sample/ApiControllers/ClientsController.cs new file mode 100644 index 0000000..8f8183a --- /dev/null +++ b/Swashbuckle.OData.Sample/ApiControllers/ClientsController.cs @@ -0,0 +1,115 @@ +using System.Data.Entity; +using System.Data.Entity.Infrastructure; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Description; +using SwashbuckleODataSample.Models; + +namespace SwashbuckleODataSample.ApiControllers +{ + public class ClientsController : ApiController + { + private readonly SwashbuckleODataContext db = new SwashbuckleODataContext(); + + // GET: api/Clients + public IQueryable GetClients() + { + return db.Clients; + } + + // GET: api/Clients/5 + [ResponseType(typeof (Client))] + public async Task GetClient(int id) + { + var client = await db.Clients.FindAsync(id); + if (client == null) + { + return NotFound(); + } + + return Ok(client); + } + + // PUT: api/Clients/5 + [ResponseType(typeof (void))] + public async Task PutClient(int id, Client client) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + if (id != client.Id) + { + return BadRequest(); + } + + db.Entry(client).State = EntityState.Modified; + + try + { + await db.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!ClientExists(id)) + { + return NotFound(); + } + throw; + } + + return StatusCode(HttpStatusCode.NoContent); + } + + // POST: api/Clients + [ResponseType(typeof (Client))] + public async Task PostClient(Client client) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + db.Clients.Add(client); + await db.SaveChangesAsync(); + + return CreatedAtRoute("DefaultApi", new + { + id = client.Id + }, client); + } + + // DELETE: api/Clients/5 + [ResponseType(typeof (Client))] + public async Task DeleteClient(int id) + { + var client = await db.Clients.FindAsync(id); + if (client == null) + { + return NotFound(); + } + + db.Clients.Remove(client); + await db.SaveChangesAsync(); + + return Ok(client); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + db.Dispose(); + } + base.Dispose(disposing); + } + + private bool ClientExists(int id) + { + return db.Clients.Count(e => e.Id == id) > 0; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/ApiControllers/Project.cs b/Swashbuckle.OData.Sample/ApiControllers/Project.cs new file mode 100644 index 0000000..f41e19e --- /dev/null +++ b/Swashbuckle.OData.Sample/ApiControllers/Project.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Web.OData.Builder; +using Microsoft.OData.Edm; + +namespace SwashbuckleODataSample.Models +{ + public class Project + { + [Key] + public int ProjectId { get; set; } + + public string ProjectName { get; set; } + + public int ClientId { get; set; } + + [ForeignKey("ClientId")] + [ActionOnDelete(EdmOnDeleteAction.Cascade)] + public Client Client { get; set; } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/ApiControllers/ProjectsController.cs b/Swashbuckle.OData.Sample/ApiControllers/ProjectsController.cs new file mode 100644 index 0000000..7cc16c7 --- /dev/null +++ b/Swashbuckle.OData.Sample/ApiControllers/ProjectsController.cs @@ -0,0 +1,115 @@ +using System.Data.Entity; +using System.Data.Entity.Infrastructure; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Description; +using SwashbuckleODataSample.Models; + +namespace SwashbuckleODataSample.ApiControllers +{ + public class ProjectsController : ApiController + { + private readonly SwashbuckleODataContext db = new SwashbuckleODataContext(); + + [Route("Projects/v1")] + public IQueryable GetProjects() + { + return db.Projects; + } + + [Route("Projects/v1/{id}")] + [ResponseType(typeof (Project))] + public async Task GetProject(int id) + { + var project = await db.Projects.FindAsync(id); + if (project == null) + { + return NotFound(); + } + + return Ok(project); + } + + [Route("Projects/v1/{id}")] + [ResponseType(typeof (void))] + public async Task PutProject(int id, Project project) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + if (id != project.ProjectId) + { + return BadRequest(); + } + + db.Entry(project).State = EntityState.Modified; + + try + { + await db.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!ProjectExists(id)) + { + return NotFound(); + } + throw; + } + + return StatusCode(HttpStatusCode.NoContent); + } + + [Route("Projects/v1")] + [ResponseType(typeof (Project))] + public async Task PostProject(Project project) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + db.Projects.Add(project); + await db.SaveChangesAsync(); + + return CreatedAtRoute("DefaultApi", new + { + id = project.ProjectId + }, project); + } + + [Route("Projects/v1/{id}")] + [ResponseType(typeof (Project))] + public async Task DeleteProject(int id) + { + var project = await db.Projects.FindAsync(id); + if (project == null) + { + return NotFound(); + } + + db.Projects.Remove(project); + await db.SaveChangesAsync(); + + return Ok(project); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + db.Dispose(); + } + base.Dispose(disposing); + } + + private bool ProjectExists(int id) + { + return db.Projects.Count(e => e.ProjectId == id) > 0; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/App_Start/FormatterConfig.cs b/Swashbuckle.OData.Sample/App_Start/FormatterConfig.cs new file mode 100644 index 0000000..8236a01 --- /dev/null +++ b/Swashbuckle.OData.Sample/App_Start/FormatterConfig.cs @@ -0,0 +1,31 @@ +using System.Web.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace SwashbuckleODataSample +{ + /// + /// + public static class FormatterConfig + { + /// + /// Registers the specified configuration. + /// + /// The configuration. + public static void Register(HttpConfiguration config) + { + var formatters = config.Formatters; + + formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DateFormatHandling = DateFormatHandling.IsoDateFormat, + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + MetadataPropertyHandling = MetadataPropertyHandling.Ignore + }; + + formatters.JsonFormatter.SerializerSettings.Converters.Add(new StringEnumConverter()); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs b/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs index 2a33550..4674416 100644 --- a/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs +++ b/Swashbuckle.OData.Sample/App_Start/SwaggerConfig.cs @@ -1,4 +1,5 @@ using System.Web.Http; +using System.Web.Http.Description; using Swashbuckle.Application; using Swashbuckle.OData; using SwashbuckleODataSample; diff --git a/Swashbuckle.OData.Sample/App_Start/WebApiConfig.cs b/Swashbuckle.OData.Sample/App_Start/WebApiConfig.cs index de850dc..d903407 100644 --- a/Swashbuckle.OData.Sample/App_Start/WebApiConfig.cs +++ b/Swashbuckle.OData.Sample/App_Start/WebApiConfig.cs @@ -16,6 +16,12 @@ public static void Register(HttpConfiguration config) // Web API routes config.MapHttpAttributeRoutes(); + config.Routes.MapHttpRoute( + name: "DefaultApi", + routeTemplate: "api/{controller}/{id}", + defaults: new { id = RouteParameter.Optional } + ); + var builder = new ODataConventionModelBuilder(); builder.EntitySet("Customers"); builder.EntitySet("Orders"); diff --git a/Swashbuckle.OData.Sample/Global.asax.cs b/Swashbuckle.OData.Sample/Global.asax.cs index ee5ee60..f14d434 100644 --- a/Swashbuckle.OData.Sample/Global.asax.cs +++ b/Swashbuckle.OData.Sample/Global.asax.cs @@ -8,6 +8,7 @@ public class WebApiApplication : HttpApplication protected void Application_Start() { GlobalConfiguration.Configure(WebApiConfig.Register); + GlobalConfiguration.Configure(FormatterConfig.Register); } } } \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/Models/SwashbuckleODataContext.cs b/Swashbuckle.OData.Sample/Models/SwashbuckleODataContext.cs deleted file mode 100644 index 357138e..0000000 --- a/Swashbuckle.OData.Sample/Models/SwashbuckleODataContext.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Data.Entity; - -namespace SwashbuckleODataSample.Models -{ - public class SwashbuckleODataContext : DbContext - { - // You can add custom code to this file. Changes will not be overwritten. - // - // If you want Entity Framework to drop and regenerate your database - // automatically whenever you change your model schema, please use data migrations. - // For more information refer to the documentation: - // http://msdn.microsoft.com/en-us/data/jj591621.aspx - - public SwashbuckleODataContext() : base("name=SwashbuckleODataContext") - { - } - - public DbSet Customers { get; set; } - - public DbSet Orders { get; set; } - } -} \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/Models/Customer.cs b/Swashbuckle.OData.Sample/ODataControllers/Customer.cs similarity index 100% rename from Swashbuckle.OData.Sample/Models/Customer.cs rename to Swashbuckle.OData.Sample/ODataControllers/Customer.cs diff --git a/Swashbuckle.OData.Sample/Controllers/CustomersController.cs b/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs similarity index 94% rename from Swashbuckle.OData.Sample/Controllers/CustomersController.cs rename to Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs index 9cb9c95..d7f55db 100644 --- a/Swashbuckle.OData.Sample/Controllers/CustomersController.cs +++ b/Swashbuckle.OData.Sample/ODataControllers/CustomersController.cs @@ -3,7 +3,9 @@ using System.Net; using System.Threading.Tasks; using System.Web.Http; +using System.Web.Http.Description; using System.Web.OData; +using System.Web.OData.Results; using SwashbuckleODataSample.Models; namespace SwashbuckleODataSample.Controllers @@ -27,6 +29,7 @@ public SingleResult GetCustomer([FromODataUri] int key) } // PUT: odata/Customers(5) + [ResponseType(typeof(void))] public async Task Put([FromODataUri] int key, Delta patch) { Validate(patch.GetEntity()); @@ -61,6 +64,7 @@ public async Task Put([FromODataUri] int key, Delta } // POST: odata/Customers + [ResponseType(typeof(Customer))] public async Task Post(Customer customer) { if (!ModelState.IsValid) @@ -75,6 +79,7 @@ public async Task Post(Customer customer) } // PATCH: odata/Customers(5) + [ResponseType(typeof(void))] [AcceptVerbs("PATCH", "MERGE")] public async Task Patch([FromODataUri] int key, Delta patch) { @@ -110,6 +115,7 @@ public async Task Patch([FromODataUri] int key, Delta Delete([FromODataUri] int key) { var customer = await _db.Customers.FindAsync(key); diff --git a/Swashbuckle.OData.Sample/Models/Order.cs b/Swashbuckle.OData.Sample/ODataControllers/Order.cs similarity index 100% rename from Swashbuckle.OData.Sample/Models/Order.cs rename to Swashbuckle.OData.Sample/ODataControllers/Order.cs diff --git a/Swashbuckle.OData.Sample/Controllers/OrdersController.cs b/Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs similarity index 78% rename from Swashbuckle.OData.Sample/Controllers/OrdersController.cs rename to Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs index 33fe67b..ed27cb2 100644 --- a/Swashbuckle.OData.Sample/Controllers/OrdersController.cs +++ b/Swashbuckle.OData.Sample/ODataControllers/OrdersController.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using System.Web.Http; +using System.Web.Http.Description; using System.Web.OData; using SwashbuckleODataSample.Models; @@ -26,41 +27,8 @@ public SingleResult GetOrder([FromODataUri] int key) return SingleResult.Create(_db.Orders.Where(order => order.OrderId == key)); } - // PUT: odata/Orders(5) - public async Task Put([FromODataUri] int key, Delta patch) - { - Validate(patch.GetEntity()); - - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - var order = await _db.Orders.FindAsync(key); - if (order == null) - { - return NotFound(); - } - - patch.Put(order); - - try - { - await _db.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) - { - if (!OrderExists(key)) - { - return NotFound(); - } - throw; - } - - return Updated(order); - } - // POST: odata/Orders + [ResponseType(typeof(Order))] public async Task Post(Order order) { if (!ModelState.IsValid) @@ -75,6 +43,7 @@ public async Task Post(Order order) } // PATCH: odata/Orders(5) + [ResponseType(typeof(void))] [AcceptVerbs("PATCH", "MERGE")] public async Task Patch([FromODataUri] int key, Delta patch) { @@ -110,6 +79,7 @@ public async Task Patch([FromODataUri] int key, Delta } // DELETE: odata/Orders(5) + [ResponseType(typeof(void))] public async Task Delete([FromODataUri] int key) { var order = await _db.Orders.FindAsync(key); diff --git a/Swashbuckle.OData.Sample/ODataControllers/SwashbuckleODataContext.cs b/Swashbuckle.OData.Sample/ODataControllers/SwashbuckleODataContext.cs new file mode 100644 index 0000000..1c97840 --- /dev/null +++ b/Swashbuckle.OData.Sample/ODataControllers/SwashbuckleODataContext.cs @@ -0,0 +1,47 @@ +using System.Data.Entity; + +namespace SwashbuckleODataSample.Models +{ + public class SwashbuckleODataContext : DbContext + { + static SwashbuckleODataContext() + { + Database.SetInitializer(new SwashbuckleODataInitializer()); + } + + + public SwashbuckleODataContext() : base("name=SwashbuckleODataContext") + { + } + + public DbSet Customers { get; set; } + + public DbSet Orders { get; set; } + + public DbSet Clients { get; set; } + + public DbSet Projects { get; set; } + } + + public class SwashbuckleODataInitializer : DropCreateDatabaseAlways + { + protected override void Seed(SwashbuckleODataContext context) + { + var clientOne = new Client { Name = "ClientOne" }; + context.Clients.Add(clientOne); + context.Clients.Add(new Client { Name = "ClientTwo" }); + + context.Projects.Add(new Project { ProjectName = "ProjectOne", Client = clientOne}); + context.Projects.Add(new Project { ProjectName = "ProjectTwo", Client = clientOne}); + + var customerOne = new Customer { Name = "CustomerOne" }; + context.Customers.Add(customerOne); + context.Customers.Add(new Customer { Name = "CustomerTwo" }); + + context.Orders.Add(new Order { OrderName = "OrderOne", Customer = customerOne }); + context.Orders.Add(new Order { OrderName = "OrderTwo", Customer = customerOne }); + + base.Seed(context); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj b/Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj index 87c1148..2994e4d 100644 --- a/Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj +++ b/Swashbuckle.OData.Sample/Swashbuckle.OData.Sample.csproj @@ -119,16 +119,21 @@ + + + + + - - + + + + Global.asax - - - + diff --git a/Swashbuckle.OData.Tests/ApplyDocumentVendorExtensions.cs b/Swashbuckle.OData.Tests/ApplyDocumentVendorExtensions.cs new file mode 100644 index 0000000..af2cd84 --- /dev/null +++ b/Swashbuckle.OData.Tests/ApplyDocumentVendorExtensions.cs @@ -0,0 +1,13 @@ +using System.Web.Http.Description; +using Swashbuckle.Swagger; + +namespace Swashbuckle.OData.Tests +{ + public class ApplyDocumentVendorExtensions : IDocumentFilter + { + public void Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer) + { + swaggerDoc.host = "foo"; + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/ContentType.cs b/Swashbuckle.OData.Tests/ContentType.cs new file mode 100644 index 0000000..5df220f --- /dev/null +++ b/Swashbuckle.OData.Tests/ContentType.cs @@ -0,0 +1,328 @@ +using System; + +namespace Swashbuckle.OData.Tests +{ + public static class ContentType + { + /// + /// Used to denote the encoding necessary for files containing JavaScript source code. The alternative MIME type for this file type is text/javascript. + /// + public const string ApplicationXJavascript = "application/x-javascript"; + + ///24bit Linear PCM audio at 8-48kHz, 1-N channels; Defined in RFC 3190 + public const string AudioL24 = "audio/L24"; + + ///Adobe Flash files for example with the extension .swf + public const string ApplicationXShockwaveFlash = "application/x-shockwave-flash"; + + ///Arbitrary binary data.[5] Generally speaking this type identifies files that are not associated with a specific application. Contrary to past assumptions by software packages such as Apache this is not a type that should be applied to unknown files. In such a case, a server or application should not indicate a content type, as it may be incorrect, but rather, should omit the type in order to allow the recipient to guess the type.[6] + public const string ApplicationOctetStream = "application/octet-stream"; + + ///Atom feeds + public const string ApplicationAtomXml = "application/atom+xml"; + + ///Cascading Style Sheets; Defined in RFC 2318 + public const string TextCss = "text/css"; + + ///commands; subtype resident in Gecko browsers like Firefox 3.5 + public const string TextCmd = "text/cmd"; + + ///Comma-separated values; Defined in RFC 4180 + public const string TextCsv = "text/csv"; + + ///deb (file format), a software package format used by the Debian project + public const string ApplicationXDeb = "application/x-deb"; + + ///Defined in RFC 1847 + public const string MultipartEncrypted = "multipart/encrypted"; + + ///Defined in RFC 1847 + public const string MultipartSigned = "multipart/signed"; + + ///Defined in RFC 2616 + public const string MessageHttp = "message/http"; + + ///Defined in RFC 4735 + public const string ModelExample = "model/example"; + + ///device-independent document in DVI format + public const string ApplicationXDvi = "application/x-dvi"; + + ///DTD files; Defined by RFC 3023 + public const string ApplicationXmlDtd = "application/xml-dtd"; + + ///ECMAScript/JavaScript; Defined in RFC 4329 (equivalent to application/ecmascript but with looser processing rules) It is not accepted in IE 8 or earlier - text/javascript is accepted but it is defined as obsolete in RFC 4329. The "type" attribute of the