C# Polymorphic models with NSwag - c#

Within my Asp.Net Core v5 application we have the following models
public class StorageRecordTypeMetadataBase
{
public string PropertyName { get; set; }
public bool Required { get; set; }
}
public class StringRecordTypeMetadata: StorageRecordTypeMetadataBase
{
public string? ValidationRegex { get; set; }
}
public class NumericRecordTypeMetadata : StorageRecordTypeMetadataBase
{
public int MinValue { get; set; }
public int MaxValue { get; set; }
}
In my application Startup.cs I have registered Swagger and NSwag setup as following:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSwaggerDocument();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseOpenApi();
app.UseSwaggerUi3();
}
}
In order to support polymorphism, I have followed the guideline as written by NSwag creator: Guide on inheritance in NSwag, and here is how my model looks like updated:
[JsonConverter(typeof(JsonInheritanceConverter), "discriminator")]
[KnownType(typeof(StringRecordTypeMetadata))]
[KnownType(typeof(NumericRecordTypeMetadata))]
public class StorageRecordTypeMetadataBase
{
public string PropertyName { get; set; }
public bool Required { get; set; }
}
As soon as I run the application, swagger fails as swagger.json could not be generated. Upon investigating the issue I can see the following error message once I try to manually navigate to /swagger/v1/swagger.json
System.MissingMethodException: Method not found: 'System.String
Namotion.Reflection.XmlDocsExtensions.GetXmlDocsSummary(System.Reflection.MemberInfo)'.
at
NSwag.Generation.Processors.OperationSummaryAndDescriptionProcessor.ProcessSummary(OperationProcessorContext
context, List1 attributes) at NSwag.Generation.Processors.OperationSummaryAndDescriptionProcessor.Process(OperationProcessorContext context) at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.RunOperationProcessors(OpenApiDocument document, ApiDescription apiDescription, Type controllerType, MethodInfo methodInfo, OpenApiOperationDescription operationDescription, List1 allOperations, OpenApiDocumentGenerator
swaggerGenerator, OpenApiSchemaResolver schemaResolver) at
NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.AddOperationDescriptionsToDocument(OpenApiDocument
document, Type controllerType, List1 operations, OpenApiDocumentGenerator swaggerGenerator, OpenApiSchemaResolver schemaResolver) at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateForControllers(OpenApiDocument document, IGrouping2[] apiGroups, OpenApiSchemaResolver
schemaResolver) at
NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateAsync(ApiDescriptionGroupCollection
apiDescriptionGroups) at
NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateAsync(Object
serviceProvider) at
NSwag.AspNetCore.OpenApiDocumentProvider.GenerateAsync(String
documentName) at
NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.GenerateDocumentAsync(HttpContext
context) at
NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.GetDocumentAsync(HttpContext
context) at
NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.GetDocumentAsync(HttpContext
context) at
NSwag.AspNetCore.Middlewares.OpenApiDocumentMiddleware.Invoke(HttpContext
context) at
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext
context)
I have tried to include the referenced package Namotion.Reflection even myself but that did not help either. Is there anything that I have missed during my configuration?
This was supposed to add discriminator field within the base model so that it would be automatically recognized when I generate my models on front end (React) side. I can achieve this behavior by moving away from NSwag, to Swashbuckle like following:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSwaggerGen(c =>
{
c.UseAllOfForInheritance();
c.SelectSubTypesUsing(baseType =>
{
return typeof(StorageRecordType).Assembly.GetTypes().Where(type => type.IsSubclassOf(baseType));
});
c.SelectDiscriminatorNameUsing((baseType) => "itemType");
c.SelectDiscriminatorValueUsing((subType) => subType.Name);
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger((SwaggerOptions c) => { });
app.UseSwaggerUI();
}
}
However this completely breaks the NSwag generation process on my React side. Methods from all controllers are put together into a single Client (instead of being separated per controller name), plus some of the classes required in the parameters seem to be gone as well.
How can I fix the NSwag in order to get the discriminator value in my swagger.json response?

Ok so the issue was, that I was using older version of Nswag.AspNetCore. Instead of version 13.10.8 I upgraded to 13.15.5, which works great with package NJsonSchema v 10.6.6

Related

ASPNET Core replacement for [Session] =

We are migrating from ASPNET MVC5 to ASPNET Core meaning we need to refactor some code.
We were using Session = model to store the model in the session, then retrieving it from another Controller.
We understand this option has been discontinued in Core.
We have attempted to use:
HttpContext.Session.SetString("Data", JsonConvert.SerializeObject(model));
However, when Deserialising by using:
var json = HttpContext.Session.GetString("Data");
var model = JsonConvert.DeserializeObject<SearchListViewModel>(json);
The resulting model does not come back the same - it is one long string rather than a structured list (which is was before Serialising).
Is there a better way to achieve passing a model from one controller to another?
To configure Session in your project, you need to do the following:
In your Startup.cs, under the Configure method, add this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSession();
}
And then under the ConfigureServices method, add this:
public void ConfigureServices(IServiceCollection services)
{
//Added for session state
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(10);
});
}
Create a model class of your object type (whatever you want to store). I am giving a simple example:
public class SearchListViewModel
{
public int SearchID{ get; set; }
public string SearchName{ get; set; }
//so on
}
Then create a SessionExtension helper to set and retrieve your complex object as JSON:
public static class SessionExtensions
{
public static void SetObjectAsJson(this ISession session, string key, object value)
{
session.SetString(key, JsonConvert.SerializeObject(value));
}
public static T GetObjectFromJson<T>(this ISession session, string key)
{
var value = session.GetString(key);
return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value);
}
}
Then finally set the complex object in your session as:
var search= new SearchListViewModel();
search.SearchID= 1;
search.SearchName= "Test";
HttpContext.Session.SetObjectAsJson("SearchListViewModel", search);
To retrieve your complex object in your session:
var searhDetails = HttpContext.Session.GetObjectFromJson<SearchListViewModel>("SearchListViewModel");
int searchID= SearchListViewModel.SearchID;
string searchName= SearchListViewModel.SearchName;
you can still use session if you need. You just need to config it in startup
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSession();
....another your code
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSession();
... another your code
}

How RoutePattern is added to an Endpoint object

Below is some simple code
public class Startup {
public void ConfigureServices(IServiceCollection services) {
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
app.UseDeveloperExceptionPage();
app.UseRouting();
app.Use(async (context, next) => {
var endpoint = context.GetEndpoint(); // <--------I put a debugger here
await next();
});
app.UseEndpoints(endpoints => {
endpoints.MapGet("routing", async context => {
await context.Response.WriteAsync("Request Was Routed");
});
});
app.Use(async (context, next) => {
await context.Response.WriteAsync("Terminal Middleware Reached");
});
}
}
When the breakpoint is hit, I can see some properties has been added to the Endpoint instance, for example, RoutePattern property
But Endpoint doesn't have a RoutePattern property:
public class Endpoint {
public Endpoint(RequestDelegate requestDelegate, EndpointMetadataCollection metadata, string displayName);
public string DisplayName { get; }
public EndpointMetadataCollection Metadata { get; }
public RequestDelegate RequestDelegate { get; }
public override string ToString();
Then how does RoutePattern property is added to the Endpoint instance?
Another question is, when I run the application, the breakpoint always get hit twice, not once, why is that, because there is only one request, the breakpoint should only hit once?
Endpoint class really doesn't have RoutePattern property, but it's child class RouteEndpoint has this property (Microsoft.AspNetCore.Routing.RouteEndpoint), when you watch it when it's debugger, you can see it.
And why hit it twice, I'm afraid there's some other code in your app leads it, in my newly created asp.net core MVC app, when the app starts, it only hit it once, but in my signalr mvc project, it hit twice because of the connection of signalr. So I think you need to check your code.
By the way, this document about middleware execution order may help.

CS0311 C# The type cannot be used as type parameter 'TContext' in the generic type or method. EntityFrameworkCore

I specify the context class I created in the entity project on the Startup.cs file and the connectionString data I created for connectionString. But why am I getting this error?
ERROR message:
Severity Code Description Project File Line Suppression State
Error CS0311 The type
'Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext'
cannot be used as type parameter 'TContext' in the generic type or
method
'EntityFrameworkServiceCollectionExtensions.AddDbContext(IServiceCollection,
Action, ServiceLifetime, ServiceLifetime)'.
There is no implicit reference conversion from
'Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext'
to
'Microsoft.EntityFrameworkCore.DbContext'. EntityFramework2 C:\Users\xsamu\source\repos\EntityFramework2\EntityFramework2\Startup.cs 29 Active
Startup class:
namespace EntityFramework2
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<UserContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DevConnection")));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
Entity configuration:
namespace EntityFramework2
{
public class EntityConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasOne<Department>(navigationExpression: s => s.Name)
.WithOne(sa => sa.User)
.HasForeignKey<Department>(sa => sa.DepartmentId);
builder.HasOne<Title>(navigationExpression: s => s.TitleCode)
.WithOne(sa => sa.User)
.HasForeignKey<Title>(sa => sa.TitleId);
builder.HasOne<Position>(navigationExpression: s => s.PositionCode)
.WithOne(sa => sa.User)
.HasForeignKey<Position>(sa => sa.PositionId);
}
}
}
There is no implicit reference conversion from 'Microsoft.ApplicationInsights.Extensibility.Implementation.UserContext' to 'Microsoft.EntityFrameworkCore.DbContext'.
The message tells you, that your UserContext class does not inherit from DbContext, which is mandatory.
It should look something like this:
public class BloggingContext : DbContext
{
public BloggingContext(DbContextOptions<BloggingContext> options)
: base(options)
{ }
public DbSet<Blog> Blogs { get; set; }
}
For further information, see the EF Core Tutorial and Configuring a DbContext.
Does Your UserContext inherit DbContext class?
Just make Your Context UserContext inherited from DbContext Class like the following Code and you are good to go:
public class UserContext : DbContext
{
public UserContext (DbContextOptions<UserContext > options)
: base(options)
{ }
public DbSet<User> Users{ get; set; }
}

Getting OData Count in ASP.NET Core WebAPI

Using the sample code from Hassan Habib's Supercharging ASP.NET Core API with OData blog post, I am able to get the record count using an OData query of $count=true:
What needs to be configured to get the response object to be wrapped in an OData context so that the #odata.count property will show?
In my own ASP.NET Core web API project, I cannot get the simple $count parameter to work and I have no idea why.
With Hassan's sample code, the response JSON is wrapped in an OData context and the payload (an IEnumerable<Student> object) is in the value property of the JSON response. In my project, the OData context wrapper does not exist; my code never returns OData context, it only returns the payload object of type IEnumerable<T>:
I've also noticed that the Content-Type in the response header is application/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8 in the sample project, where as it is simply application/json; charset=utf-8 in my project. I don't see any setting that controls this in either project, so I'm assuming the Microsoft.AspNetCore.Odata NuGet package is magically changing the response when it's configured properly.
My project is also using .NET Core 2.2 (Upgraded from 2.1), all the same versions of NuGet packages as Hassan's sample projects, and all the same settings in the StartUp.cs class... although my StartUp.cs is way more complicated (hence the reason I'm not posting it's content here.)
I could reproduce your issue when i use [Route("api/[controller]")]and [ApiController] with the startup.cs like below:
app.UseMvc(routeBuilder =>
{
routeBuilder.Expand().Select().Count().OrderBy().Filter();
routeBuilder.EnableDependencyInjection();
});
To fix it,be sure you have built a private method to do a handshake between your existing data models (OData model in this case) and EDM.
Here is a simple demo:
1.Controller(comment on Route attribute and ApiController attribute):
//[Route("api/[controller]")]
//[ApiController]
public class StudentsController : ControllerBase
{
private readonly WSDbContext _context;
public StudentsController(WSDbContext context)
{
_context = context;
}
// GET: api/Students
[HttpGet]
[EnableQuery()]
public IEnumerable<Student> Get()
{
return _context.Students;
}
}
//[Route("api/[controller]")]
//[ApiController]
public class SchoolsController : ControllerBase
{
private readonly WSDbContext _context;
public SchoolsController(WSDbContext context)
{
_context = context;
}
// GET: api/Schools
[HttpGet]
[EnableQuery()]
public IEnumerable<School> Get()
{
return _context.Schools;
}
2.Startup.cs():
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore(action => action.EnableEndpointRouting = false);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
var connection = #"Server=(localdb)\mssqllocaldb;Database=WSDB;Trusted_Connection=True;ConnectRetryCount=0";
services.AddDbContext<WSDbContext>(options => options.UseSqlServer(connection));
services.AddOData();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc(routeBuilder =>
{
routeBuilder.Expand().Select().Count().OrderBy().Filter();
routeBuilder.MapODataServiceRoute("api", "api", GetEdmModel());
});
}
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Student>("Students");
builder.EntitySet<Student>("Schools");
return builder.GetEdmModel();
}
}
Just been battling this.
I found that if I request my controller at /api/Things that most of the OData options work but $count doesn't.
However, $count does work if I request the same method via /odata/Things.
In my case I wanted to extend existing Api methods with [EnableQuery] but have it include the count metadata.
I ended up extending the EnableQuery attribute to return a different reponse, it worked perfectly.
public class EnableQueryWithMetadataAttribute : EnableQueryAttribute
{
public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
if (actionExecutedContext.Result is ObjectResult obj && obj.Value is IQueryable qry)
{
obj.Value = new ODataResponse
{
Count = actionExecutedContext.HttpContext.Request.ODataFeature().TotalCount,
Value = qry
};
}
}
public class ODataResponse
{
[JsonPropertyName("#odata.count")]
public long? Count { get; set; }
[JsonPropertyName("value")]
public IQueryable Value { get; set; }
}
}
You can just set an empty preffix route when you map OData, and you will receive OData with your request your endpoint.
routeBuilder.MapODataServiceRoute("ODataEdmModel", "", GetEdmModel());
In my case I've created a special action named $count that users the OData Filter query my collection (EF Database) and return the Count;
[HttpGet("$count")]
public async Task<int> Count(ODataQueryOptions<MyBookEntity> odataQueryOptions)
{
var queryable = this.dataContext.MyBooks;
return await odataQueryOptions.Filter
.ApplyTo(queryable, new ODataQuerySettings())
.Cast<MyBookEntity>()
.CountAsync();
}

ASP.NET Core 2 Unable to resolve service for type Microsoft EntityFrameworkCore DbContext

When I run my asp.net core 2 projects I get the following error message:
InvalidOperationException: Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContext' while attempting to activate 'ContosoUniversity.Service.Class.StudentService'.
Here is my project structure:
-- solution 'ContosoUniversity'
----- ContosoUniversity
----- ContosoUniversity.Model
----- ContosoUniversity.Service
IEntityService (related code) :
public interface IEntityService<T> : IService
where T : BaseEntity
{
Task<List<T>> GetAllAsync();
}
IEntityService (related code) :
public abstract class EntityService<T> : IEntityService<T> where T : BaseEntity
{
protected DbContext _context;
protected DbSet<T> _dbset;
public EntityService(DbContext context)
{
_context = context;
_dbset = _context.Set<T>();
}
public async virtual Task<List<T>> GetAllAsync()
{
return await _dbset.ToListAsync<T>();
}
}
Entity :
public abstract class BaseEntity {
}
public abstract class Entity<T> : BaseEntity, IEntity<T>
{
public virtual T Id { get; set; }
}
IStudentService :
public interface IStudentService : IEntityService<Student>
{
Task<Student> GetById(int Id);
}
StudentService :
public class StudentService : EntityService<Student>, IStudentService
{
DbContext _context;
public StudentService(DbContext context)
: base(context)
{
_context = context;
_dbset = _context.Set<Student>();
}
public async Task<Student> GetById(int Id)
{
return await _dbset.FirstOrDefaultAsync(x => x.Id == Id);
}
}
SchoolContext :
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
}
And finally here is my Startup.cs class :
public class Startup
{
public Startup(IConfiguration configuration, IHostingEnvironment env, IServiceProvider serviceProvider)
{
Configuration = configuration;
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
Configuration = builder.Build();
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<SchoolContext>(option =>
option.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<IStudentService, StudentService>();
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
What should I do to resolve this problem?
StudentService expects DbContext but the container does not know how to resolve it based on your current startup.
You would need to either explicitly add the context to the service collection
Startup
services.AddScoped<DbContext, SchoolContext>();
services.AddScoped<IStudentService, StudentService>();
Or update the StudentService constructor to explicitly expect a type the container knows how to resolve.
StudentService
public StudentService(SchoolContext context)
: base(context)
{
//...
}
I encountered a similar error i.e.
An unhandled exception occurred while processing the request.
InvalidOperationException: Unable to resolve service for type 'MyProjectName.Models.myDatabaseContext' while attempting to activate 'MyProjectName.Controllers.MyUsersController'.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, bool isDefaultParameterRequired)
What I later figured out was... I was missing the following line i.e. adding my database context to services:
services.AddDbContext<yourDbContext>(option => option.UseSqlServer("Server=Your-Server-Name\\SQLExpress;Database=yourDatabaseName;Trusted_Connection=True;"));
Here goes my ConfigureServices method defined in Startup class:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential
//cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddDbContext<yourDbContext>(option =>
option.UseSqlServer("Server=Your-Server-Name\\SQLExpress;Database=yourDatabaseName;Trusted_Connection=True;"));
}
...
...
}
Basically, when you generated model classes from database, all your database tables were mapped into respective Model classes by creating the "New Scaffolded Item" and choosing the appropriate database context during the scaffolding procedure.
Now, you need to manually register your database context as a service to the services parameter of ConfigureServices method.
Btw, rather than hard coding your connection string, you'll ideally pick it up from the configuration data. I have attempted to keep things simple here.
if dbcontext inherited from system.data.entity.DbContext then it woud be added like that
services.AddScoped(provider => new CDRContext());
services.AddTransient<IUnitOfWork, UnitOfWorker>();
services.AddTransient<ICallService, CallService>();
This error is thrown when the options argument is null or cannot be retrieved using GetConnectionString().
I had this error because my appsettings.json file that defines my ConnectionStrings had an extra curly bracket } at the end.
Stupid, but frustrating.

Categories

Resources