-
Notifications
You must be signed in to change notification settings - Fork 23
Joker.OData
Boilerplate code for OData web services. Please help out the community by sharing your suggestions and code improvements:
Install-Package Joker.OData
- Built in Autofac IoC container for dependency injection
- Built in serialization with NewtonsoftJson
- ODataStartup Kestrel self hosting and IIS integration
- ODataController GET, POST, PUT, DELETE, CreateRef and DeleteRef implementation
- ErrorLoggerMiddleware
- Enabled ODataBatchHandler. Support for SaveChangesOptions.BatchWithSingleChangeset with SQL Server database transaction. This means end to end transaction scope from your .NET clients.
- Serilog
- Both EntityFrameworkCore and EntityFramework are supported
- Health check support
Manage extensions: download "AspNetCore OData EF Joker template" or download free vsix installer from marketplace https://marketplace.visualstudio.com/items?itemName=tomasfabian.SelfHostedODataService-Joker-EF
Menu: File->New->Project...
Add new project from template named SelfHostedODataService.Joker.EF (.Net Core)
EF Core project template name:
AspNetCore OData EFCore Joker template
using Joker.OData.Hosting;
public class StartupBaseWithOData : Joker.OData.Startup.ODataStartup
{
public StartupBaseWithOData(IWebHostEnvironment env)
: base(env)
{
}
}
public class Program
{
public static async Task Main(string[] args)
{
//var startupSettings = new KestrelODataWebHostConfig()
var startupSettings = new IISODataWebHostConfig()
{
ConfigureServices = services =>
{
services.AddHostedService<SqlTableDependencyProviderHostedService>();
}
};
await new ODataHost<StartupBaseWithOData>().RunAsync(args, startupSettings);
}
}
There are two OData server configuration options one for Kestrel and one for IIS integration:
- KestrelODataWebHostConfig
- IISODataWebHostConfig
private static IISODataWebHostConfig ODataStartupConfigExample()
{
var configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
//all settings are optional
var startupSettings = new IISODataWebHostConfig()
{
ConfigureServices = services =>
{
services.AddHostedService<SqlTableDependencyProviderHostedService>();
},
Urls = new[] { @"https://localhost:32778/" },
Configuration = configuration
};
return startupSettings;
}
How to call not configured webHostBuilder extensions:
public class ODataHostExample : ODataHost<StartupBaseWithOData>
{
protected override void OnConfigureWebHostBuilder(IWebHostBuilder webHostBuilder)
{
webHostBuilder.CustomExtension();
}
}
Calling of webHostBuilder.Build inside OnConfigureWebHostBuilder is not recommended. ODataHost.Run will call it as the final step instead of you.
public class StartupBaseWithOData : ODataStartup
{
public StartupBaseWithOData(IWebHostEnvironment env)
: base(env)
{
}
protected override ODataModelBuilder OnCreateEdmModel(ODataModelBuilder oDataModelBuilder)
{
oDataModelBuilder.Namespace = "Example";
oDataModelBuilder.EntitySet<Product>("Products");
oDataModelBuilder.AddPluralizedEntitySet<Book>();
return oDataModelBuilder;
}
private void ConfigureNLog()
{
var config = new LoggingConfiguration();
// ...
}
protected override void OnConfigureServices(IServiceCollection services)
{
ConfigureNLog();
}
protected override void RegisterTypes(ContainerBuilder builder)
{
ContainerBuilder.RegisterModule(new ProductsAutofacModule());
ContainerBuilder.RegisterType<ProductsConfigurationProvider>()
.As<IProductsConfigurationProvider>()
.SingleInstance();
ContainerBuilder.RegisterType<SchedulersFactory>().As<ISchedulersFactory>()
.SingleInstance();
}
protected override void OnConfigureApp(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime)
{
base.OnConfigureApp(app, env, applicationLifetime); //Registers routeBuilder.MapRoute("WebApiRoute", "api/{controller}/{action}/{id?}");
}
}
Base class for OData CRUD operations. Supported operations are GetAll, FindByKey, Create, Update, Patch, Delete and CreateRef. You can intercept all CRUD operations, see delete entity example below.
public class ProductsController : ODataControllerBase<Product>
{
public ProductsController(IRepository<Product> repository)
: base(repository)
{
}
protected override Task<int> OnDelete(int key)
{
//intercept delete entity example
return base.OnDelete(key);
}
}
In order to find the related entity during AddLink or SetLink you have to provide the corresponding DbSet from the DbContext for the current http request (scope) in TryGetDbSet override:
public class AuthorsController : ODataControllerBase<Author>
{
private readonly ISampleDbContext dbContext;
public AuthorsController(IRepository<Author> repository, ISampleDbContext dbContext)
: base(repository)
{
this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
}
protected override dynamic TryGetDbSet(Type entityType)
{
if (entityType == typeof(Book))
return dbContext.Books;
return null;
}
}
ReadOnlyODataController<TEntity>
does not expose CUD operations.
- DeleteRef
- TransactionScopeODataBatchHandler
If you need end point routing please use ODataStartup, otherwise you have to switch to ODataStartupLegacy
public class StartupBaseWithOData : ODataStartupLegacy
ODataStartupLegacy disables end point routing:
services.AddMvc(options =>
{
options.EnableEndpointRouting = false;
})
and maps OData route with:
app.UseMvc(routeBuilder =>
In order the get database transactions you have to register IDbTransactionFactory
protected override void RegisterTypes(ContainerBuilder builder)
{
base.RegisterTypes(builder);
builder.RegisterType<SampleDbContext>()
.As<ISampleDbContext, IDbTransactionFactory, IContext>()
.WithParameter(connectionStringParameter)
.InstancePerLifetimeScope();
}
DbContextBase implements IDbTransactionFactory and is inherited from System.Data.Entity.DbContext so you could change your code like this
public partial class SampleDbContext : DbContextBase //:DbContext
You can change the default SQL Server IsolationLevel.RepeatableRead in the following way
protected override ODataBatchHandler OnCreateODataBatchHandler()
{
var batchHandler = (TransactionScopeODataBatchHandler)base.OnCreateODataBatchHandler();
batchHandler.BatchDbIsolationLevel = IsolationLevel.ReadCommitted;
return batchHandler;
}
In this example all inserts are commited to the database together or rollback-ed in case of any error.
try
{
var dataServiceContext = new ODataServiceContextFactory().Create(url);
dataServiceContext.AddObject("Authors", new Author() { LastName = new Random().Next(1, 100000).ToString()});
dataServiceContext.AddObject("Authors", new Author() { LastName = "Asimov"});
dataServiceContext.AddObject("Authors", new Author() { LastName = new Random().Next(1, 100000).ToString()});
var dataServiceResponse = await dataServiceContext.SaveChangesAsync(SaveChangesOptions.BatchWithSingleChangeset);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
Install-Package Joker.EntityFrameworkCore
using Joker.Contracts.Data;
using Joker.EntityFrameworkCore.Database;
using Joker.EntityFrameworkCore.Repositories;
public interface ITestDbContext : IContext, IDbTransactionFactory
{
DbSet<Product> Products { get; set; }
}
public class TestDbContext : DbContextBase, ITestDbContext
{
public DbSet<Product> Products { get; set; }
}
public class ProductsRepository : Repository<Product>
{
private readonly ITestDbContext context;
public ProductsRepository(ITestDbContext context)
: base(context)
{
this.context = context ?? throw new ArgumentNullException(nameof(context));
}
protected override DbSet<Product> DbSet => context.Products;
}
or Install-Package Joker.EntityFramework
Note: EntityFramework uses IDbSet<TEntity>
interface instead of DbSet<TEntity>
abstract class as in EF Core
protected override IDbSet<Product> DbSet => context.Products;
DesignTimeDbContextFactory reads the connection string from your appsettings.json during Add-Migration and Update-Database commands
"ConnectionStrings": {
"FargoEntities": "Server=127.0.0.1,1402;User Id = SA;Password=<YourNewStrong@Passw0rd>;Initial Catalog = Test;MultipleActiveResultSets=true"
}
using Sample.DataCore.EFCore;
using Joker.EntityFrameworkCore.DesignTime;
public class DesignTimeDbContextFactory : DesignTimeDbContextFactory<SampleDbContextCore>
{
public DesignTimeDbContextFactory()
{
ConnectionStringName = "FargoEntities"; // default ConnectionStringName is set to DefaultConnection
}
protected override SampleDbContextCore Create(DbContextOptions<SampleDbContextCore> options)
{
return new SampleDbContextCore(options);
}
}
public StartupBaseWithOData(IWebHostEnvironment env)
: base(env)
{
SetSettings(startupSettings =>
{
startupSettings
.DisableHttpsRedirection(); //by default it is enabled
startupSettings.UseDeveloperExceptionPage = false; //by default it is enabled
});
SetODataSettings(odataStartupSettings =>
{
odataStartupSettings
//OData route prefix setup https://localhost:5001/odata/$metadata
.SetODataRouteName("odata") //default is empty https://localhost:5001/$metadata
.DisableODataBatchHandler(); //by default it is enabled
});
SetWebApiSettings(webApiStartupSettings =>
{
webApiStartupSettings
.SetWebApiRoutePrefix("myApi") //default is "api"
.SetWebApiTemplate("api/{controller}/{id}"); //default is "{controller}/{action}/{id?}"
});
}
OData client examples:
dataServiceContext.AddObject("Books", book);
dataServiceContext.UpdateObject(book);
dataServiceContext.DeleteObject(book);
dataServiceContext.AttachTo("Books", book);
dataServiceContext.SetLink(book, "Publisher", publisher);
dataServiceContext.SetLink(book, "Publisher", null); //Remove navigation property reference
dataServiceContext.AddLink(author, "Books", book);
dataServiceContext.DeleteLink(author, "Books", book);
Filter books by id and include all authors:
https://localhost:5001/Books('New%20Id')?$expand=Authors
ErrorLoggerMiddleware intercepts all exceptions and log them with ILogger provided by ILoggerFactory. Default logger for OData hosts is ConsoleLogger.
You can override the default query validation settings:
protected override ODataValidationSettings OnCreateODataValidationSettings()
{
var oDataValidationSettings = base.OnCreateODataValidationSettings();
oDataValidationSettings.MaxExpansionDepth = 3; //default is 2
oDataValidationSettings.AllowedQueryOptions = //disabled AllowedQueryOptions.Format
AllowedQueryOptions.Apply | AllowedQueryOptions.SkipToken | AllowedQueryOptions.Count
| AllowedQueryOptions.Skip | AllowedQueryOptions.Top | AllowedQueryOptions.OrderBy
| AllowedQueryOptions.Select | AllowedQueryOptions.Expand | AllowedQueryOptions.Filter;
return oDataValidationSettings;
}
Added support for web apis without OData configuration:
public class ApiStartup : Joker.OData.Startup.ApiStartup
{
public ApiStartup(IWebHostEnvironment env)
: base(env, enableEndpointRouting : true)
{
SetSettings(s =>
{
s.UseAuthentication = false;
s.UseAuthorization = false;
});
}
protected override void OnConfigureServices(IServiceCollection services)
{
base.OnConfigureServices(services);
services.AddSingleton<ICarService, CarService>();
}
}
public class Program
{
public static async Task Main(string[] args)
{
var webHostConfig = new IISWebHostConfig();
await new ApiHost<ApiStartup>().RunAsync(args, webHostConfig);
}
}
https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-3.1
Basic health check https://localhost:5001/healthCheck can be overriden:
protected override string HealthCheckPath { get; } = "/healthCheck"; // override default /health
protected override IEndpointConventionBuilder OnMapHealthChecks(IEndpointRouteBuilder endpoints)
{
var healthCheckOptions = new HealthCheckOptions
{
AllowCachingResponses = false,
ResultStatusCodes =
{
[HealthStatus.Healthy] = StatusCodes.Status200OK,
[HealthStatus.Degraded] = StatusCodes.Status200OK,
[HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
}
};
return endpoints.MapHealthChecks(HealthCheckPath, healthCheckOptions)
.RequireAuthorization();
}
or extended:
protected override IEndpointConventionBuilder OnMapHealthChecks(IEndpointRouteBuilder endpoints)
{
return base.OnMapHealthChecks(endpoints).RequireAuthorization();
}
- Autofac.Extensions.DependencyInjection 5.0.1 updated to 7.0.2. See https://github.com/autofac/Autofac/releases/tag/v6.0.0 and bellow
protected override void OnRegisterEdmModel(IServiceCollection services)
{
services.AddSingleton(EdmModel); //in this case, it is same as base.OnRegisterEdmModel(services)
}
public Startup(IWebHostEnvironment env)
: base(env)
{
SetSettings(startupSettings =>
{
startupSettings.UseCors = true;
});
}
Configure cors policies
protected override void OnConfigureCorsPolicy(CorsOptions options)
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("https://localhost:5003", "http://localhost:3000")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
options.DefaultPolicyName = MyAllowSpecificOrigins;
}
protected override void OnConfigureCorsPolicy(CorsOptions options)
{
//Sets AllowAnyOrigin, AllowAnyMethod and AllowAnyHeader.
AddDefaultCorsPolicy(options);
}
-
ODataStartupConfig was renamed to ODataWebHostConfig
-
KestrelODataStartupConfig was renamed to KestrelODataWebHostConfig
-
IISHostedODataStartupConfig was renamed to IISODataWebHostConfig
-
ODataStartup namespace Joker.OData changed to Joker.OData.Startup
-
Repository and ReadOnlyRepository were moved to Joker.EntityFramework.Repositories