Rendering multiple Razor pages to Html - asp.net-core

I have an application that uses Markdown in Razor Pages (compiled using a custom TagHelper) to display static content. There are about 300 pages worth.
I'm trying to render these Razor pages to html so that I can index the content using Lucene.net, in order to provide full text search.
I've managed to create some code (see below) that works, but prepends previously rendered content whenever subsequent pages are rendered. That is, when I render the first page it works perfectly but, when I render another page the first page's content is also included and, when I render a third page the first two pages content are included.
I have tried creating the dependencies manually within the method, and I have tried creating a new instance of this class for each page, but always with the same result.
What am I missing?
public class RazorPageRenderer
{
private readonly IRazorViewEngine razorViewEngine;
private readonly ITempDataProvider tempDataProvider;
private readonly IHttpContextAccessor httpContextAccessor;
private readonly IActionContextAccessor actionContextAccessor;
private readonly IRazorPageActivator razorPageActivator;
private readonly ILogger logger;
public RazorPageRenderer(
IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IHttpContextAccessor httpContextAccessor,
IActionContextAccessor actionContextAccessor,
IRazorPageActivator razorPageActivator,
ILogger<RazorPageRenderer> logger)
{
this.razorViewEngine = razorViewEngine;
this.tempDataProvider = tempDataProvider;
this.httpContextAccessor = httpContextAccessor;
this.actionContextAccessor = actionContextAccessor;
this.razorPageActivator = razorPageActivator;
this.logger = logger;
}
public async Task<string> RenderAsync(IRazorPage razorPage) => await RenderAsync<object>(razorPage, null);
public async Task<string> RenderAsync<T>(IRazorPage razorPage, T model)
{
try
{
var viewDataDictionary = CreateViewDataDictionary(razorPage.GetType(), model);
await using var writer = new StringWriter();
var view = new RazorView(
razorViewEngine,
razorPageActivator,
Array.Empty<IRazorPage>(),
razorPage,
HtmlEncoder.Default,
new DiagnosticListener(nameof(RazorPageRenderer)));
var viewContext = new ViewContext(
actionContextAccessor.ActionContext,
view,
viewDataDictionary,
new TempDataDictionary(
httpContextAccessor.HttpContext,
tempDataProvider),
writer,
new HtmlHelperOptions());
if (razorPage is Page page)
{
page.PageContext = new PageContext(actionContextAccessor.ActionContext)
{
ViewData = viewContext.ViewData
};
}
razorPage.ViewContext = viewContext;
razorPageActivator.Activate(razorPage, viewContext);
await razorPage.ExecuteAsync();
return writer.ToString();
}
catch (Exception exception)
{
logger.LogError(
"An exception occured while rendering page: {Page}. Exception: {Exception}",
razorPage.Path,
exception);
}
return null;
}
private static ViewDataDictionary CreateViewDataDictionary(Type pageType, object model)
{
var dictionaryType = typeof(ViewDataDictionary<>)
.MakeGenericType(pageType);
var ctor = dictionaryType.GetConstructor(new[]
{typeof(IModelMetadataProvider), typeof(ModelStateDictionary)});
var viewDataDictionary = (ViewDataDictionary)ctor?.Invoke(
new object[] {new EmptyModelMetadataProvider(), new ModelStateDictionary()});
if (model != null && viewDataDictionary != null)
{
viewDataDictionary.Model = model;
}
return viewDataDictionary;
}
}

Related

Caching odata Web Api

I am developing an OData API for my Asp.net core application and i want to implement caching on this.
The problem is all my endpoints will be IQueryable with a queryable services with no execution at all. so i can't implement any caching on service level
Controller
public class TagsController : ODataController
{
private readonly ITagService _tagService;
private readonly ILogger<TagsController> _logger;
public TagsController(ITagService tagService, ILogger<TagsController> logger)
{
_tagService = tagService;
_logger = logger;
}
[HttpGet("odata/tags")]
[Tags("Odata")]
[AllowAnonymous]
[EnableCachedQuery]
public ActionResult<IQueryable<Tag>> Get()
{
try
{
return Ok(_tagService.GetAll());
}
catch (Exception ex)
{
_logger.LogError(ex, "Some unknown error has occurred.");
return BadRequest();
}
}
}
So I tried to apply an extension on EnableQuery attribute to add the caching implementation on it. so i added the following
public class EnableCachedQuery : EnableQueryAttribute
{
private IMemoryCache _memoryCache;
public EnableCachedQuery()
{
_memoryCache = new MemoryCache(new MemoryCacheOptions());
}
public override void OnActionExecuting(ActionExecutingContext actionContext)
{
//var url = GetAbsoluteUri(actionContext.HttpContext);
var path = actionContext.HttpContext.Request.Path + actionContext.HttpContext.Request.QueryString;
//check cache
if (_memoryCache.TryGetValue(path, out ObjectResult value))
{
actionContext.Result = value;
}
else
{
base.OnActionExecuting(actionContext);
}
}
public override void OnActionExecuted(ActionExecutedContext context)
{
if (context.Exception != null)
return;
var path = context.HttpContext.Request.Path + context.HttpContext.Request.QueryString;
var cacheEntryOpts = new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(15));
base.OnActionExecuted(context);
_memoryCache.Set(path, context.Result, cacheEntryOpts);
}
}
the first request completed successfully and retrieved the data correctly with filters and queries applied. then when tried to add the data to cache the context.Result holds the ObjectResult and then in the second request which should be cached the value was there but with an error in executing which means that the cached value is not the final output value that should be passed to the Result
Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
Object name: 'ApplicationDbContext'.
============================
Update:
public class ApplicationDbContext : IdentityDbContext<User, Account, Session>, IApplicationDbContext
{
public ApplicationDbContext(
DbContextOptions options,
IApplicationUserService currentUserService,
IDomainEventService domainEventService,
IBackgroundJobService backgroundJob,
IDomainEventService eventService,
IDateTime dateTime) : base(options, currentUserService, domainEventService, backgroundJob, dateTime) { }
public DbSet<Tag> Tags => Set<Tag>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
var entityTypes = builder.Model.GetEntityTypes()
.Where(c => typeof(AuditableEntity).IsAssignableFrom(c.ClrType))
.ToList();
foreach (var type in entityTypes)
{
var parameter = Expression.Parameter(type.ClrType);
var deletedCheck = Expression.Lambda
(Expression.Equal(Expression.Property(parameter, nameof(AuditableEntity.Deleted)), Expression.Constant(false)), parameter);
type.SetQueryFilter(deletedCheck);
}
builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
builder.ApplySeedsFromAssembly(typeof(ApplicationDbContext).Assembly);
}
}

Sending cshtml view mails with mailkit

I am using ASP.NET Core and Mailkit to send emails. Take the following (Basic) code:
var bodyBuilder = new BodyBuilder();
bodyBuilder.HtmlBody = GetBody();
var m = new MimeMessage();
m.To.Add(new MailboxAddress("gurdip.sira#gmail.com"));
m.From.Add(new MailboxAddress("Sender Name", "gurdip.sira#gmail.com"));
string s = GetBody();
// m.Body = bodyBuilder.ToMessageBody();
m.Body = new TextPart(MimeKit.Text.TextFormat.Html) {Text = s};
using (var smtp = new MailKit.Net.Smtp.SmtpClient())
{
smtp.Connect("smtp.gmail.com", 587);
smtp.AuthenticationMechanisms.Remove("XOAUTH2");
smtp.Authenticate("gurdip.sira#gmail.com", "December5!");
smtp.Send(m);
}
The GetBody() method just reads a html document (streamreader).
What I'd like to do is use razor views and cshtml as my emails may contain dynamic content (e.g. an unknown sized collection of certain items).
I can't seem to find definitive documentation on how to do this. The idea is to then just read the cshtml view as plain html but resolve the razor syntax and model variables.
Anyone done anything like this?
One solution is from your controller to pass the content.
public void TestAction(){
var content = PartialView("your_partial_view").ToString();
your_SendEmailFunction(content)
}
So basically you use the partial view as a string that you pass as a content to your method.
Here is a simple demo based on jmal73's comment in Paris Polyzos' blog like below:
1.custom interface:
public interface IViewRenderService
{
Task<string> RenderToStringAsync(string viewName, object model);
}
2.implement interface:
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
public class ViewRenderService : IViewRenderService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly HttpContext _httpContext;
public ViewRenderService(IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IHttpContextAccessor httpContextAccessor)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_httpContext = httpContextAccessor.HttpContext;
}
public async Task<string> RenderToStringAsync(string viewName, object model)
{
var actionContext = new ActionContext(_httpContext, new RouteData(), new ActionDescriptor());
var viewEngineResult = _razorViewEngine.FindView(actionContext, viewName, false);
if (viewEngineResult.View == null || (!viewEngineResult.Success))
{
throw new ArgumentNullException($"Unable to find view '{viewName}'");
}
var view = viewEngineResult.View;
using (var sw = new StringWriter())
{
var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
viewDictionary.Model = model;
var tempData = new TempDataDictionary(_httpContext, _tempDataProvider);
var viewContext = new ViewContext(actionContext, view, viewDictionary, tempData, sw, new HtmlHelperOptions());
viewContext.RouteData = _httpContext.GetRouteData(); //set route data here
await view.RenderAsync(viewContext);
return sw.ToString();
}
}
}
3.read .cshtml file and return string:
public class HomeController : Controller
{
private readonly IViewRenderService _viewRenderService;
public HomeController(IViewRenderService viewRenderService)
{
_viewRenderService = viewRenderService;
}
public IActionResult Index()
{
var data = new Users()
{
UserId = 1
};
return View(data);
}
public async Task<IActionResult> Privacy()
{
var data = new Users()
{
UserId = 1
};
var result = await _viewRenderService.RenderToStringAsync("Home/Index", data);
return Content(result);
}
4.Index.cshtml:
#model Users
<form>
<label asp-for="UserId"></label>
<br />
<input asp-for="UserId" class="form-control" maxlength="4" />
<span asp-validation-for="UserId" class="text-danger"></span>
<input type="submit" value="create" />
</form>
5.Register service:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IViewRenderService, ViewRenderService>();

Blazor recaptcha validation attribute IHttpContextAccessor is always null

I thought I would have a go at using Blazor server-side, and so far I've managed to overcome most headaches one way or another and enjoyed it, until now.
I'm trying to write a validator for Google Recaptcha v3, which requires a users IP address. Usually I would just get the IHttpContextAccessor with:
var httpContextAccessor = (IHttpContextAccessor)validationContext.GetService(typeof(IHttpContextAccessor));
But that now returns null! I also found that trying to get IConfiguration in the same way failed, but for that, I could just make a static property in Startup.cs.
This is the last hurdle in a days work, and it's got me baffled.
Any ideas on how to get that IP address into a validator?
Thanks!
Edit:
I just found the error making httpContextAccessor null!
((System.RuntimeType)validationContext.ObjectType).DeclaringMethodthrew an exception of type 'System.InvalidOperationException'
this is the validator:
public class GoogleReCaptchaValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
Lazy<ValidationResult> errorResult = new Lazy<ValidationResult>(() => new ValidationResult("Google reCAPTCHA validation failed", new String[] { validationContext.MemberName }));
if (value == null || String.IsNullOrWhiteSpace(value.ToString()))
{
return errorResult.Value;
}
var configuration = Startup.Configuration;
string reCaptchResponse = value.ToString();
string reCaptchaSecret = configuration["GoogleReCaptcha:SecretKey"];
IHttpContextAccessor httpContextAccessor = validationContext.GetService(typeof(IHttpContextAccessor)) as IHttpContextAccessor;
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("secret", reCaptchaSecret),
new KeyValuePair<string, string>("response", reCaptchResponse),
new KeyValuePair<string, string>("remoteip", httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString())
});
HttpClient httpClient = new HttpClient();
var httpResponse = httpClient.PostAsync("https://www.google.com/recaptcha/api/siteverify", content).Result;
if (httpResponse.StatusCode != HttpStatusCode.OK)
{
return errorResult.Value;
}
String jsonResponse = httpResponse.Content.ReadAsStringAsync().Result;
dynamic jsonData = JObject.Parse(jsonResponse);
if (jsonData.success != true.ToString().ToLower())
{
return errorResult.Value;
}
return ValidationResult.Success;
}
}
For this issue, it is caused by that when DataAnnotationsValidator call AddDataAnnotationsValidation, it did not pass IServiceProvider to ValidationContext.
For this issue, you could check Make dependency resolution available for EditContext form validation so that custom validators can access services. #11397
private static void ValidateModel(EditContext editContext, ValidationMessageStore messages)
{
var validationContext = new ValidationContext(editContext.Model);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true);
// Transfer results to the ValidationMessageStore
messages.Clear();
foreach (var validationResult in validationResults)
{
foreach (var memberName in validationResult.MemberNames)
{
messages.Add(editContext.Field(memberName), validationResult.ErrorMessage);
}
}
editContext.NotifyValidationStateChanged();
}
For a workaround, you could implement your own DataAnnotationsValidator and AddDataAnnotationsValidation .
Follow steps below:
Custom DataAnnotationsValidator
public class DIDataAnnotationsValidator: DataAnnotationsValidator
{
[CascadingParameter] EditContext DICurrentEditContext { get; set; }
[Inject]
protected IServiceProvider ServiceProvider { get; set; }
protected override void OnInitialized()
{
if (DICurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)} " +
$"inside an EditForm.");
}
DICurrentEditContext.AddDataAnnotationsValidationWithDI(ServiceProvider);
}
}
Custom EditContextDataAnnotationsExtensions
public static class EditContextDataAnnotationsExtensions
{
private static ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo> _propertyInfoCache
= new ConcurrentDictionary<(Type, string), PropertyInfo>();
public static EditContext AddDataAnnotationsValidationWithDI(this EditContext editContext, IServiceProvider serviceProvider)
{
if (editContext == null)
{
throw new ArgumentNullException(nameof(editContext));
}
var messages = new ValidationMessageStore(editContext);
// Perform object-level validation on request
editContext.OnValidationRequested +=
(sender, eventArgs) => ValidateModel((EditContext)sender, serviceProvider, messages);
// Perform per-field validation on each field edit
editContext.OnFieldChanged +=
(sender, eventArgs) => ValidateField(editContext, serviceProvider, messages, eventArgs.FieldIdentifier);
return editContext;
}
private static void ValidateModel(EditContext editContext, IServiceProvider serviceProvider,ValidationMessageStore messages)
{
var validationContext = new ValidationContext(editContext.Model, serviceProvider, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true);
// Transfer results to the ValidationMessageStore
messages.Clear();
foreach (var validationResult in validationResults)
{
foreach (var memberName in validationResult.MemberNames)
{
messages.Add(editContext.Field(memberName), validationResult.ErrorMessage);
}
}
editContext.NotifyValidationStateChanged();
}
private static void ValidateField(EditContext editContext, IServiceProvider serviceProvider, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier)
{
if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
{
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
var validationContext = new ValidationContext(fieldIdentifier.Model, serviceProvider, null)
{
MemberName = propertyInfo.Name
};
var results = new List<ValidationResult>();
Validator.TryValidateProperty(propertyValue, validationContext, results);
messages.Clear(fieldIdentifier);
messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage));
// We have to notify even if there were no messages before and are still no messages now,
// because the "state" that changed might be the completion of some async validation task
editContext.NotifyValidationStateChanged();
}
}
private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, out PropertyInfo propertyInfo)
{
var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo))
{
// DataAnnotations only validates public properties, so that's all we'll look for
// If we can't find it, cache 'null' so we don't have to try again next time
propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
// No need to lock, because it doesn't matter if we write the same value twice
_propertyInfoCache[cacheKey] = propertyInfo;
}
return propertyInfo != null;
}
}
Replace DataAnnotationsValidator with DIDataAnnotationsValidator
<EditForm Model="#starship" OnValidSubmit="#HandleValidSubmit">
#*<DataAnnotationsValidator />*#
<DIDataAnnotationsValidator />
<ValidationSummary />
</EditForm>
For IHttpContextAccessor, you need to register in Startup.cs like
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddHttpContextAccessor();
}

Asp.Net Core 2.0 Xunit Tests

I am new .asp.net core. I am testing a controller that renders a view to a string and then utilises evo pdf to render the view.
All is working perfectly and I am also able to successfully test using postman.
However my test app errors when I use vs 2017 test explorer to debug my test (Xunit).
Searched Locations within the razor engine
The error occurs within my RenderViewToString method as my razor view engine is unable to locate the view to render. The paths searched to locate the views are as expected. Any guidance is appreciated.
//Unit Test Code
[Fact]
public async void GetPdf()
{
var response = await _client.PostAsJsonAsync<Common.DTO.Invoice>("/api/values/1", GetDummyData());
using (var file = System.IO.File.Create(#"c:\\Test" + DateTime.Now.ToString("yyyyyMMddHHmmss") + ".pdf"))
{
//create a new file to write to
await response.Content.CopyToAsync(file);
await file.FlushAsync(); // flush back to disk before disposing
}
}
//Render view to string service
public interface IViewRenderService
{
Task<string> RenderToStringAsync(string viewName, ViewDataDictionary viewData);
}
public class ViewRenderService : IViewRenderService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public ViewRenderService(IRazorViewEngine razorViewEngine,ITempDataProvider tempDataProvider,IServiceProvider serviceProvider)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public async Task<string> RenderToStringAsync(string viewName, ViewDataDictionary viewData)
{
var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
using (var sw = new StringWriter())
{
var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);
if (viewResult.View == null)
{
throw new ArgumentNullException($"{viewName} does not match any available view");
}
var viewContext = new ViewContext(
actionContext,
viewResult.View,
viewData,
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
sw,
new HtmlHelperOptions()
);
await viewResult.View.RenderAsync(viewContext);
return sw.ToString();
}
}
}
I was getting the same error with core 2.0. The problem is RazorViewEngine is not working as expected with empty RouteData object;
So i injected IHttpContextAccessor and got HttpContext and RouteData from it;
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IViewRenderService, ViewRenderService>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddMvc();
}
RazorToStringHelper.cs:
public interface IViewRenderService
{
Task<string> RenderToStringAsync(string viewName, object model);
}
public class ViewRenderService : IViewRenderService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
public ViewRenderService(
IRazorViewEngine razorViewEngine,
IHttpContextAccessor httpContextAccessor,
ITempDataProvider tempDataProvider)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_httpContextAccessor = httpContextAccessor;
}
public async Task<string> RenderToStringAsync(string viewName, object model)
{
var httpContext = _httpContextAccessor.HttpContext;
var actionContext = new ActionContext(httpContext, httpContext.GetRouteData(), new ActionDescriptor());
var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);
if (viewResult.View == null)
{
throw new ArgumentNullException($"{viewName} does not match any available view");
}
using (var sw = new StringWriter())
{
var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = model
};
var viewContext = new ViewContext(
actionContext,
viewResult.View,
viewDictionary,
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
sw,
new HtmlHelperOptions()
);
await viewResult.View.RenderAsync(viewContext);
return sw.ToString();
}
}
}
Due to time constraints I abandoned the XUnit approach, wrote a test app and also
utilised postman as this was an api requirement to render a pdf from a razor view.

asp.net-core least steps to create and save new entry from API

I want to take a few post query parameters from an API i have and create a new entry. I wanted to do this with in the method with out needing to load context or something.
namespace fais.printing_services.Controllers
{
[Produces("application/json")]
[Route("api/[controller]/[action]")]
public class printController : Controller
{
private readonly IHostingEnvironment _appEnvironment;
public printController(IHostingEnvironment appEnvironment)
{
_appEnvironment = appEnvironment;
}
/**/
[HttpPost]
public IActionResult request(string id="test_default", string url = "", string html = "")
{
print_job _print_job = new print_job();
_print_job.html = html;
_print_job.options = options; //json object string
_print_job.url = url;
using (ApplicationDbContext db = new ApplicationDbContext())
{
db.print_job.Add(_print_job);
db.SaveChanges();
}
return Json(new
{
save = true
});
}
}
}
I just want to be able create a new print_job entry and save it when the API is called and return a json response.
Add ApplicationDbContext to controller constructor, it will be injected automatically (if your Startup.cs is like recommeneded):
private readonly IHostingEnvironment _appEnvironment;
private readonly ApplicationDbContext _db;
public printController(IHostingEnvironment appEnvironment, ApplicationDbContext db)
{
_appEnvironment = appEnvironment;
_db = db;
}
[HttpPost]
public IActionResult request(string id="test_default", string url = "", string html = "")
{
var _print_job = new print_job()
{
html = html,
options = options,
url = url,
}
_db.print_job.Add(_print_job);
_db.SaveChanges();
return Json(new { save = true });
}